18
votes

I have a problem while drawing with both GDI and GDI+ interchangeably. The page transformation—in particular scaling—seems to be a little bit off between the two. Which properties of the GDI context affects the scaling of the output other than SetViewportExt and SetWindowExt?

The code uses almost exclusively GDI for its drawing, but uses GDI+ in a few cases where its features (semitransparency) are needed. It uses SetViewportExt, SetWindowExt and SetViewportOrg to enable zooming and scrolling.

When GDI+ is needed I construct a Gdiplus::Graphics object around the HDC and do the drawing. I assume this makes the graphics context wrap the device context and relay its rendering to the device context. If I extract the transformation matrix of the GDI+ graphics context, I see that it is the identity matrix, so the scaling is done elsewhere (in the device context I guess).

I devised a simple test where I draw the same array of rectangles with GDI and GDI+ to be sure that all the transformations are the same in both cases. The code snippet follows:

CRect rect = ...;

// Draw the rectangle using GDI
CPen cpen(PS_DASH, 0, RGB(0,0,255));
pDC->SelectObject(&cpen);
pDC->Rectangle(rect);

{
    // Draw the rectangle using GDI+
    Gdiplus::Graphics graphics(pDC->m_hDC);

    Gdiplus::Pen pen(Gdiplus::Color(180,180,180));
    graphics.DrawRectangle(
        &pen,
        Gdiplus::Rect(rect.left, rect.top, rect.Width(), rect.Height()));
}

And the result is here: (the blue dashed is drawn by GDI and the gray is drawn by GDI+)

Result drawn by code

I can clearly see that the two coordinate systems are different. I expected some round-off errors, but not a scaling error as seen here. Also, when I change the zoom factor, the GDI+ jumps around ±4 pixel in both directions depending on the zoom. This is also highlighted in the screenshot as the GDI+ rectangle has a positive offset on the X axis and a negative offset on the Y axis as compared to the GDI rectangle.

  • Does anybody know what's going on here?

  • How would I go about investigating/debugging this? This happens in the bowels of windows so I'm unfortunately unable to debug it.

For reference, this is what my viewport/window org/ext looks like:

Window Ext: (134000, 80500)
Window Org: (0, 0)
Viewport Ext: (1452 872)
Viewport Org: (35 35)

Update:

I have fixed the problem, but it's not pretty. The basic approach is:

  1. Take two coordinates (origin and a second appropriate point) in screen space and transform them to logical coordinates using GDI (DPtoLP function).

  2. Reset the GDI transformation to MM_TEXT.

  3. Use the transformed points to construct a transformation matrix for GDI+ which represent the same transformation

  4. And finally use this matrix to construct a GDI+ context with the correct transformation.

This is a bit of a hack, but it works. I still don't know why there is a difference between the two, though. At least it goes to show that it is possible to have a GDI+ context mimic the GDI transformation.

4

4 Answers

6
votes

Short answer: call graphics.SetPageUnit(Gdiplus::UnitPixel)

I had the same problem as https://stackoverflow.com/a/4894969/700027: when printing, the coordinates for GDI+ (Gdiplus::Graphics) did not match the coordinates from GDI (HDC).

graphics.GetPageUnit() was returning UnitDisplay. The documentation of UnitDisplay is:

Specifies display units. For example, if the display device is a monitor, then the unit is 1 pixel.

I had wrongly assumed that for a printer, UnitDisplay would use printer dots. After much struggle I finally found out that it was actually using 1/100 of inches for an unknown reason. If I use Gdiplus::UnitPixel then GDI+ coordinates are the same as GDI coordinates.

2
votes

We have had the same problem.

(Background: GDI works fine for just about everything, and seems to be much faster for our spreadsheet-style displays with 1000's of cells of text that need to be rendered. However we need GDI+ for displaying .jpg's.)

GDI+ scaling seemed correct when displaying stuff on the screen. We have a Print Preview feature which uses a coordinate transformation to let the app render using printer coordinates but to appear on the screen. Everything worked fine until we sent it to an actual printer (or PDF writer) when the scaling got stuffed up.

After a man-week's worth of work (and getting a hint from you with your solution), this is our understanding:

There is a bug in GDI+ that when you call: "Graphics(HDC)" (create a Graphics object from a GDI device context), where the HDC is from a printer or software printer with e.g. 6000 x 4000 pixel resolution, then GDI+ ignores the fact that the HDC is working with this large resolution and instead it applies its own resolution of about 1000 x 800 pixels.

Therefore, your workaround is probably the correct and best solution to the problem.

Our solution is similar but a little different, on the grounds that we don't actually want any coordinate transforms:

        graphics.GetVisibleClipBounds(&rect);
        double deltaY = (double)GetPrinterH()/(double)rect.Height;
        double deltaX = (double)GetPrinterW()/(double)rect.Width;
        x1=x1/deltaX;
        x2=x2/deltaX;
        y1=y1/deltaY;
        y2=y2/deltaY;
        graphics.DrawImage(jpeg->image, x1,y1,x2-x1,y2-y1);

These scaling factors seem to be very close to '6' on many printer drivers.

0
votes

I found a workaround for the issues when printing. Note that the graphics object in the example doesn't set any world space transformations, so drawing is done directly in page space.

Setting the page units to inches, then converting coordinates to inches seems to fix the drawing issues without much additional work. Tested with display and printer DCs at different DPIs (ranging from 72 to 4000.)

Gdiplus::Graphics graphics(..);
Gdiplus::RectF    rect(0.0f, 0.0f, 1.0f, 1.0f);
Gdiplus::REAL     dpiX = graphics.getDpiX();
Gdiplus::REAL     dpiY = graphics.getDpiY();

/* Logical coordinates to inches. In this example, the window extents are
   equal to the DC's DPI. You will have to convert to inches based on your
   specific configuration. */
rect.X      /= dpiX;
rect.Y      /= dpiY;
rect.Width  /= dpiX;
rect.Height /= dpiY;

graphics.SetPageUnit(Gdiplus::UnitInch);
graphics.FillRectangle(.., rect);
-1
votes

One thing to remember is that most of GDI usually runs on the hardware (i.e. GDI functions map to the display driver that implement some functionality on silicon) GDI+ was supposed to get hardware acceleration, but it remained as a software alone renderer.

Try manually setting a few pixels via GDI+ and GDI and see if they differ.

Perhaps the way your particular graphics card transforms co-ordinates deviates from the way it happens in GDI+