create graphics of entire screen

2k views Asked by At

I ame using a class to increase and decrease the gamma of all my screens, then i start the program and increase or decrease the gamma it works fine, but after a while (20 seconds or so) it does not work anymore, ive located the problem and it seems to be the Graphics.FromHwnd(IntPtr.Zero).GetHdc().ToInt32(); i need to refresh this and then it works again. in the example code this is only done once during initialisation, but to make it work i have pasted the line inside the SetBrightness() method, so everytime it is refreshed. is it ok to do it like this or can i expect problems?

This is the code:

public static class Brightness
{
    [DllImport("gdi32.dll")]
    private unsafe static extern bool SetDeviceGammaRamp(Int32 hdc, void* ramp);

    private static bool initialized = false;
    private static Int32 hdc;

    private static void InitializeClass()
    {
        if (initialized)
            return;

        //Get the hardware device context of the screen, we can do
        //this by getting the graphics object of null (IntPtr.Zero)
        //then getting the HDC and converting that to an Int32.
        hdc = Graphics.FromHwnd(IntPtr.Zero).GetHdc().ToInt32();

        initialized = true;
    }

    public static unsafe bool SetBrightness(short brightness)
    {
        InitializeClass();

        hdc = Graphics.FromHwnd(IntPtr.Zero).GetHdc().ToInt32();

        if (brightness > 255)
            brightness = 255;

        if (brightness < 0)
            brightness = 0;

        short* gArray = stackalloc short[3 * 256];
        short* idx = gArray;

        for (int j = 0; j < 3; j++)
        {
            for (int i = 0; i < 256; i++)
            {
                int arrayVal = i * (brightness + 128);

                if (arrayVal > 65535)
                    arrayVal = 65535;

                *idx = (short)arrayVal;
                idx++;
            }
        }

        //For some reason, this always returns false?
        bool retVal = SetDeviceGammaRamp(hdc, gArray);

        //Memory allocated through stackalloc is automatically free'd
        //by the CLR.

        return retVal;

    }
}

This is how it is called:

short gammaValue = 128;

    void gammaUp_OnButtonDown(object sender, EventArgs e)
    {
        if (gammaValue < 255)
        {
            gammaValue += 10;
            if (gammaValue > 255)
                gammaValue = 255;
            Brightness.SetBrightness(gammaValue);
        }
    }

    void gammaDown_OnButtonDown(object sender, EventArgs e)
    {
        if (gammaValue > 0)
        {
            gammaValue -= 10;
            if (gammaValue < 0)
                gammaValue = 0;
            Brightness.SetBrightness(gammaValue);
        }
    }
4

There are 4 answers

2
Jim Jeffries On

I don't see any problem specifically with using Graphics.FromHwnd(IntPtr.Zero).GetHdc().ToInt32(); twice, although I would expect the value to be the same both times, unless something external to your program is affecting it.

I would be more concerned about the following section always returning false.

 //For some reason, this always returns false?
 bool retVal = SetDeviceGammaRamp(hdc, gArray);

The documentation specifies

Return Value

If this function succeeds, the return value is TRUE.

If this function fails, the return value is FALSE.

so I would say that something is not working correctly for that call to always return false.

1
El Zorko On

I'd guess this is because the device context handle returned by GetHdc() must be released after use, it's assumed that you'll do this as soon as possible; you should be able to release it immediately after calling SetDeviceGammaRamp() via ReleaseHdc(). Essentially any time you get a DC you should release it, and asap.

C# is providing only a thin veneer here over the Win32 GDI APIs, so the rules which apply are those of GetDC() and ReleaseDC(), and you could pinvoke GetDC() with IntPtr.Zero to the same effect (and with the same issues).

The GDI is picky like this, you're only "borrowing" the handles and those you get are allocated are generally from a pool of a certain size, and once it runs out, you don't get any more DCs as you haven't released the previous ones. The same is true if one repeatedly allocates GDI brushes or pens to paint with; eventually you run out. Further, retaining pooled handles generally doesn't work out well; I can't tell you exactly what the problem is with this inside the GDI, but I've hit similar issues before and barring driver problems I'd expect your approach to work if you get the DC, set the ramp, then release the DC immediately.

In passing, there are exceptions to these pairwise rules as noted in MSDN, for example for private DCs assigned to a window via Win32's CS_OWNDC; those don't come from the pool, so you can keep them as long as you like.

Incidentally, unless you're forcing x86 builds I'd suggest using IntPtr instead of Int32 in SetDeviceGammaRamp(), since this is a pointer-sized integer rather than an always 32-bit value. And rather than void*, I'd define the RAMP structure or MarshalAs a fixed-length array. But I don't think either of these are causing your problem. Whilst you're at it, you could switch to calling GetDC() and ReleaseDC() via pinvoke and cut out a Graphics object you're not using. PInvoke.net has (at time of writing) a safer definition of SetDeviceGammaRamp() as well as GetDC() and ReleaseDC().

4
γηράσκω δ' αεί πολλά διδασκόμε On

If you want to see the error code when your code fails you can use:

[DllImport("kernel32.dll")]
public static extern uint GetLastError();

bool retVal = SetDeviceGammaRamp(hdc, gArray);
if (retVal == false)
{
    System.Console.WriteLine(GetLastError());
}

error code is 87: ERROR_INVALID_PARAMETER

Clearly the hdc is the invalid parameter. This happens because in your:

hdc = Graphics.FromHwnd(IntPtr.Zero).GetHdc()

Graphics.FromHwnd(IntPtr.Zero) is the graphic object from which you get the hdc. This object doesn't exist anymore thus the error.

Getting an hdc: hdc = Graphics.FromHwnd(IntPtr.Zero).GetHdc() every time the SetBrightness function is called solves the problem but it crashes (as you said in a comment) by calling Graphics.FromHwnd(IntPtr.Zero).ReleaseHdc();. This happens because Graphics.FromHwnd(IntPtr.Zero) is not the same as the first one.

SOLUTION

There are many:

1. Create the graphics object and get from it the dc handle every time SetBrightness is called, release the resources in the end:

public static unsafe bool SetBrightness(short brightness)
{
    Graphics gr;
    gr = Graphics.FromHwnd(IntPtr.Zero);
    IntPtr hdc = gr.GetHdc(); //Use IntPtr instead of Int32, don't need private static Int32 hdc;

    ...
    ...

    //For some reason, this always returns false?
    bool retVal = SetDeviceGammaRamp(hdc, gArray);

    //Memory allocated through stackalloc is automatically free'd
    //by the CLR.

    gr.ReleaseHdc();        
    gr.Dispose();

    return retVal;
}

2. Create the graphics object and get from it the dc handle once in InitializeClass() function, release the resources in the end of the program:

private static IntPtr hdc;
private static Graphics gr;

private static void InitializeClass()
{
    if (initialized)
        return;

    gr = Graphics.FromHwnd(IntPtr.Zero);
    hdc = gr.GetHdc();

    initialized = true;
}

public static unsafe bool SetBrightness(short brightness)
{
    InitializeClass();

    //hdc = Graphics.FromHwnd(IntPtr.Zero).GetHdc(); //You don't need it since we get the hdc once.

    ...
    ...

    //For some reason, this always returns false?
    bool retVal = SetDeviceGammaRamp(hdc, gArray);

    //Memory allocated through stackalloc is automatically free'd
    //by the CLR.

    return retVal;
}

3. Use GetDC api:

[DllImport("user32.dll")]
public static extern IntPtr GetDC(IntPtr hWnd);

[DllImport("user32.dll")]
public static extern bool ReleaseDC(IntPtr hWnd, IntPtr hDC);

private static IntPtr hdc;

private static void InitializeClass()
{
    if (initialized)
        return;

    hdc = GetDC(IntPtr.Zero);

    initialized = true;
}

public static unsafe bool SetBrightness(short brightness)
{
    InitializeClass();

    //hdc = Graphics.FromHwnd(IntPtr.Zero).GetHdc(); //You don't need it since we get the hdc once.

    ...
    ...

    //For some reason, this always returns false?
    bool retVal = SetDeviceGammaRamp(hdc, gArray);

    //Memory allocated through stackalloc is automatically free'd
    //by the CLR.

    return retVal;
}

In the end of the program release hdc:

ReleaseDC(IntPtr.Zero, hdc);
5
Simon Thum On

This is a very simple, classic memory management problem.

You need to keep a reference to

Graphics.FromHwnd(IntPtr.Zero)

around, otherwise the garbage collector (incidentially GC too) will think you are not using your Graphics Context (GC).

Keeping a ref around means retaining a valid reference, somtimes called "live object". In your class Brightness case it translates to (from memory, not compiled!)

private static bool initialized = false;
private static Graphics gc;

private static void InitializeClass()
{
    if (initialized)
        return;

    //Get the hardware device context of the screen, we can do
    //this by getting the graphics object of null (IntPtr.Zero)
    gc = Graphics.FromHwnd(IntPtr.Zero);

    initialized = true;
}

and later you invoke SetRamp(gc.GetHdc().ToInt32(), ...);

The crucial point is that as long as Brightness is initialized, the Graphics object is needed, so the garbage collector will not free it.

You were only remembering it's numeric handle value, and when calling ReleaseDC() explicitly you were doing the runtime's job, crashing when the runtime does the same.