0

I am using WinForms with C#. I have a form that its opacity is set to X. I want to draw a filled rectangle that its opacity is Y.

If I draw it like this:

private void Form1_Paint(object sender, PaintEventArgs e)
{
    e.Graphics.FillRectangle(
        new SolidBrush(Color.FromArgb(Y, 255, 0, 0)),//Red brush (Y opacity)
        new Rectangle(100,100,500,500)
        );
}

then, the Y value I have for the opacity of the brush for the rectangle drawing is getting multiplied by the X opacity of my form, so instead I get X*Y.

I think it has to do with e.Graphics, which comes from the form. How can I go about this ?

dimitris93
  • 4,155
  • 11
  • 50
  • 86
  • does this help: http://stackoverflow.com/questions/7580145/draw-a-fill-rectangle-with-low-opacity – danish Apr 24 '15 at 07:30
  • Not using the image. If you are setting the opacity of brush between 0-255, you can make your rectangle as transparent as you want compared to container (the form in this case) – danish Apr 24 '15 at 07:31
  • @danish I know that. The problem is I get `X*Y` opacity. When I want to get only `Y` opacity. Thats what I am asking... [This question](http://stackoverflow.com/questions/21009054/draw-opaque-graphics-on-transparent-form-c-sharp-winforms) is more close to what I am looking for – dimitris93 Apr 24 '15 at 07:33
  • If when you set to `Y` you get `Y*X` just set it to `Y/X` you'll get `(Y/X)*X` which is `Y` – Thomas Ayoub Apr 24 '15 at 07:38
  • @Thomas Then I will have to set it to 400......... – dimitris93 Apr 24 '15 at 07:39
  • What's this `400` you're talking about ? Where does it come ? – Thomas Ayoub Apr 24 '15 at 07:39
  • 1
    AFAIk, you cannot draw something on a form which is more opaque than form itself. Opacity is relative to container and is the outer limit for contained controls. – danish Apr 24 '15 at 07:43
  • @danish That's the purpose of my question. There has to be a workaround for this. – dimitris93 Apr 24 '15 at 07:44
  • Yes there is. It will involve lot of work. Search for "per pixel alpha bend". I guess that should work out for you. – danish Apr 24 '15 at 07:46

2 Answers2

1

You're not drawing to the screen, you're drawing to an underlying surface. The controls and surfaces are hierarchical, so if you draw something with 100% opacity on something with 50%, the result will be 50% opacity. This is simply how desktop composition works. Unlike, say, Photoshop, there's not 30 different composition modes you could exploit, it's all just "normal layers".

The opacity of the form is not hidden in the Graphics element. Instead, you're drawing your nice 100% opaque rectangle on a transparent surface; the result is a 100% opaque rectangle on a transparent surface. But then this surface is drawn on a surface with 50% opacity - and opacity multiplies. To change this, you would have to change the way the whole form is rendered, not just your control.

The only way around this would be to use a more hacky way to make your form transparent - for example, forcing everything except your rectangle to be drawn with different opacity, including the form borders etc. This is obviously kind of hard - there's no .NET methods to help you do so, you'll have to work with WinAPI directly, override some window message handling etc. It's just way too much work.

There are workarounds, of course. For example, you could show your rectangle on a different, border-less form, that moves the same as the underlying form.

Luaan
  • 62,244
  • 7
  • 97
  • 116
  • ok, I know what you mean because I have been struggling with this for days. The way I worked around it is by making a whole new form as the "rectangle". Now the program is as follows. User clicks to point A, drags it point B and the rectangle gets updated constantly and drawn from A to B (its a snippet tool). The problem with this is that the form randomly freezes... Do you have any idea why this happens ? – dimitris93 Apr 24 '15 at 07:53
  • another idea would be to use `Graphics g = Graphics.FromHwndInternal(IntPtr.Zero);` then the rectangle starts flickering. Do you know how to solve any of the 2 above ideas I had so that I can solve my problem ? – dimitris93 Apr 24 '15 at 07:55
  • @Shiro Well, your case is actually one where using a new form is not even a hacky solution - it's the obvious solution, really :D Random freezes are quite unlikely. Are you running in a debugger? When you experience a "freeze", just pause the debugging immediately and have a look at what the UI thread is doing (you have just one, right?) – Luaan Apr 24 '15 at 07:55
  • The mouse is resizing the form, so naturally it is never on the form. I am using a [global mouse key hook](https://github.com/gmamaladze/globalmousekeyhook) library to get the mouse because it is always outside of the form. So, my code is just a few likes of Mouse events handling, and resizing the form, what exactly is there to debug ? I don't know how to do that. When I used `Graphics g = Graphics.FromHwndInternal(IntPtr.Zero);` and I drew on a 1920x1080 transparent form, the form never froze, because the mouse was always on the form (?) – dimitris93 Apr 24 '15 at 08:00
  • Also, when the form freezes, the program is still running, and the "global mouse key hook" library doesn't suppress the mouse events as it used to – dimitris93 Apr 24 '15 at 08:05
  • @Shiro Whenever your UI freezes, it's because the UI thread is too busy to handle windows messages. By pausing the execution, you can have a look at the UI thread and find what it's doing. – Luaan Apr 24 '15 at 08:05
  • Problem with that is I don't know how to do that, and even if I did, I wouldn't know what to do to solve that. Considering that my code consists of this library I am talking about and only resizing a form and drawing a rectangle, as I said... – dimitris93 Apr 24 '15 at 08:07
  • @Shiro And using global mouse hooks really isn't all that good of an idea. You can use WndProc overrides, it's still easier than redrawing the whole form manually - http://stackoverflow.com/questions/1535826/resize-borderless-window-on-bottom-right-corner/17220049#17220049 – Luaan Apr 24 '15 at 08:08
  • @Shiro As for the `Graphics.FromHwndInternal(IntPtr.Zero)`, it flickers because you're drawing without having an associated form - as soon as you move mouse over that area, the form that's actually visible in that area will be redrawn. So you're fighting every time the mouse moves over something the underlying form wants to redraw. – Luaan Apr 24 '15 at 08:11
  • what do you mean by redrawing the whole form manually ? I am changing this.Size to resize it – dimitris93 Apr 24 '15 at 08:11
  • @Shiro You can just implement the code I've linked to. That said, if you want to receive mouse events while dragging from a form (e.g. while moving or resizing your form), just make it capture the mouse - `Form.Capture = true`. If you already have the rest of the code, this will make it work well. – Luaan Apr 24 '15 at 08:16
1

You can use UpdateLayeredWindow() API to draw with per pixel alpha. First make your window without borders and add the WS_EX_LAYERED style:

protected override CreateParams CreateParams
{
    get
    {
        // Add the layered extended style (WS_EX_LAYERED) to this window
        CreateParams createParam = base.CreateParams;
        createParam.ExStyle = (createParam.ExStyle | WS_EX_LAYERED);
        return createParam;
    }
}

You do all your drawings on a 32bpp bitmap and finally call the UpdateLayeredBitmap function:

private void UpdateLayeredBitmap(Bitmap bitmap)
    {
    // Does this bitmap contain an alpha channel?
    if (bitmap.PixelFormat != System.Drawing.Imaging.PixelFormat.Format32bppArgb)
    {
        //"The bitmap must be 32bpp with alpha-channel."
    }

    // Get device contexts
    IntPtr screenDc = GetDC(IntPtr.Zero);
    IntPtr memDc = CreateCompatibleDC(screenDc);
    IntPtr hBitmap = IntPtr.Zero;
    IntPtr hOldBitmap = IntPtr.Zero;

    try
    {            
        // Get handle to the new bitmap and select it into the current device context
        hBitmap = bitmap.GetHbitmap(Color.FromArgb(0));
        hOldBitmap = SelectObject(memDc, hBitmap);

        newLocation = new POINT(this.Location.X, this.Location.Y);

        // Update the window
        UpdateLayeredWindow(Handle, IntPtr.Zero, ref newLocation, ref newSize, memDc, ref sourceLocation, 0, ref blend, ULW_ALPHA);
    }

    catch (Exception e)
    {
        Console.WriteLine("{0} Exception caught.", e);
    }

    if (hBitmap != IntPtr.Zero)
    {
        SelectObject(memDc, hOldBitmap);
        // Remove bitmap resources
        DeleteObject(hBitmap);
    }
    DeleteDC(memDc);
    DeleteDC(screenDc);
}

For an example I am using a 500x500 form, and the the color green with two opacity values X = 50 and Y = 180

Point pnt;
private POINT sourceLocation = new POINT (0, 0);
private SIZE newSize = new SIZE(500, 500); //Form size
private POINT newLocation;

BLENDFUNCTION blend;
public const int ULW_ALPHA = 2;
public const byte AC_SRC_OVER = 0;
public const byte AC_SRC_ALPHA = 1;
public const int WS_EX_LAYERED = 524288;
public const int WM_NCLBUTTONDOWN = 0xA1;
public const int HTCAPTION = 0x2;

public Form1()
{
    InitializeComponent();

    this.Location = new Point(200, 200);
    bmp = new Bitmap(500, 500, System.Drawing.Imaging.PixelFormat.Format32bppArgb);

    //the form region
    Region greenRegionOpacityX = new Region(new Rectangle (0, 0, 500, 500));

    Graphics g;
    g = Graphics.FromImage(bmp);
    g.Clear(Color.Transparent);
    g.FillRegion(new SolidBrush(Color.FromArgb(50, 0, 255, 0)), greenRegionOpacityX);
    g.Dispose();
    greenRegionOpacityX.Dispose();

    blend = new BLENDFUNCTION();

    // Only works with a 32bpp bitmap
    blend.BlendOp = AC_SRC_OVER;
    // Always 0
    blend.BlendFlags = 0;
    // Set to 255 for per-pixel alpha values
    blend.SourceConstantAlpha = 255;
    // Only works when the bitmap contains an alpha channel
    blend.AlphaFormat = AC_SRC_ALPHA;

    UpdateLayeredBitmap(bmp);
}

private void Form1_MouseDown(object sender, MouseEventArgs e)
{
    if (e.Button == System.Windows.Forms.MouseButtons.Left)
    {
        pnt = new Point(e.X, e.Y);
    }
}

private void Form1_MouseMove(object sender, MouseEventArgs e)
{
    long start;
    long stop;
    long frequency;
    double elapsedTime;

    if (e.Button == System.Windows.Forms.MouseButtons.Left)
    {
        QueryPerformanceFrequency(out frequency);
        QueryPerformanceCounter(out start);

        //the form region
        Region greenRegionOpacityX = new Region(new Rectangle(0, 0, 500, 500));
        //we exclude the rectangle that we draw with the mouse
        greenRegionOpacityX.Exclude(new Rectangle(Math.Min(pnt.X, e.X), Math.Min(pnt.Y, e.Y),
                                                      Math.Abs(pnt.X - e.X), Math.Abs(pnt.Y - e.Y)));
        //the rectangle that we draw with the mouse
        Region greenRegionOpacityY = new Region(new Rectangle(Math.Min(pnt.X, e.X), Math.Min(pnt.Y, e.Y),
                                                                  Math.Abs(pnt.X - e.X), Math.Abs(pnt.Y - e.Y)));

        Graphics g;
        g = Graphics.FromImage(bmp);
        g.Clear(Color.Transparent); //we always clear the bitmap

        //draw the two regions
        g.FillRegion(new SolidBrush(Color.FromArgb(50, 0, 255, 0)), greenRegionOpacityX);
        g.FillRegion(new SolidBrush(Color.FromArgb(180, 0, 255, 0)), greenRegionOpacityY);

        g.Dispose();
        greenRegionOpacityX.Dispose();
        greenRegionOpacityY.Dispose();

        //upadate the layered window
        UpdateLayeredBitmap(bmp);

       QueryPerformanceCounter(out stop);

       elapsedTime = ((double)(stop - start) * 1000000.0d) / (double)frequency;
       Console.WriteLine("{0} μsec", elapsedTime);
       //In my pc it is ~12msec
    }
}

And these API:

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

[DllImport("gdi32.dll", EntryPoint = "CreateCompatibleDC", SetLastError=true)]
static extern IntPtr CreateCompatibleDC([In] IntPtr hdc);

[DllImport("gdi32.dll", EntryPoint = "SelectObject")]
static extern IntPtr SelectObject([In] IntPtr hdc, [In] IntPtr hgdiobj);

[DllImport("user32.dll", ExactSpelling = true, SetLastError = true)]
static extern bool UpdateLayeredWindow(IntPtr hwnd, IntPtr hdcDst,
    ref POINT pptDst, ref SIZE psize, IntPtr hdcSrc, ref POINT pptSrc, uint crKey, 
        [In] ref BLENDFUNCTION pblend, uint dwFlags);

[DllImport("gdi32.dll", EntryPoint = "DeleteDC")]
static extern bool DeleteDC([In] IntPtr hdc);

[DllImport("gdi32.dll", EntryPoint = "DeleteObject")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool DeleteObject([In] IntPtr hObject);

[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
    public int X;
    public int Y;

    public POINT(int x, int y)
    {
        this.X = x;
        this.Y = y;
    }
}

[StructLayout(LayoutKind.Sequential)]
public struct SIZE
{
    public int cx;
    public int cy;

    public SIZE(int cx, int cy)
    {
        this.cx = cx;
        this.cy = cy;
    }
}

public struct BLENDFUNCTION
{
    public byte BlendOp;

    public byte BlendFlags;

    public byte SourceConstantAlpha;

    public byte AlphaFormat;
}

EDIT (Fast with GDI)

I used QueryPerformanceCounter for the measurements:

[DllImport("KERNEL32")]
private static extern bool QueryPerformanceCounter(out long lpPerformanceCount);

[DllImport("Kernel32.dll")]
private static extern bool QueryPerformanceFrequency(out long lpFrequency);

long start;
long stop;
long frequency;
double elapsedTime;

QueryPerformanceFrequency(out frequency);
QueryPerformanceCounter(out start);

//code for time measurement

QueryPerformanceCounter(out stop);

elapsedTime = ((double)(stop - start) * 1000000.0d) / (double)frequency;
Console.WriteLine("{0} μsec", elapsedTime); //in micro seconds!

The GDI+ approach ~12 msec (see the updated Form1_MouseMove).

GDI:

[System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
private static extern int BitBlt(
      IntPtr hdcDest,     // handle to destination DC (device context)
      int nXDest,         // x-coord of destination upper-left corner
      int nYDest,         // y-coord of destination upper-left corner
      int nWidth,         // width of destination rectangle
      int nHeight,        // height of destination rectangle
      IntPtr hdcSrc,      // handle to source DC
      int nXSrc,          // x-coordinate of source upper-left corner
      int nYSrc,          // y-coordinate of source upper-left corner
      int dwRop  // raster operation code
      );

public const int SRCCOPY = 0x00CC0020;

IntPtr hdcMemTransparent = IntPtr.Zero, hdcMemGreenOpacityX = IntPtr.Zero, 
       hdcMemGreenOpacityY = IntPtr.Zero, hdcMemForm = IntPtr.Zero;
IntPtr hBitmapTransparent = IntPtr.Zero, hBitmapGreenOpacityX = IntPtr.Zero,
       hBitmapGreenOpacityY = IntPtr.Zero, hBitmapForm = IntPtr.Zero;
IntPtr hBitmapTransparentOld = IntPtr.Zero, hBitmapGreenOpacityXOld = IntPtr.Zero, 
       hBitmapGreenOpacityYOld = IntPtr.Zero, hBitmapFormOld = IntPtr.Zero;

Bitmap bitmapTransparent, bitmapGreenOpacityX, bitmapGreenOpacityY, bitmapForm;

public Form1()
{
    InitializeComponent();

    this.Location = new Point(200, 200);

    bitmapForm = new Bitmap(500, 500, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
    bitmapTransparent = new Bitmap(500, 500, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
    bitmapGreenOpacityX = new Bitmap(500, 500, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
    bitmapGreenOpacityY = new Bitmap(500, 500, System.Drawing.Imaging.PixelFormat.Format32bppArgb);

    Graphics g;

    g = Graphics.FromImage(bitmapTransparent);
    g.Clear(Color.Transparent);
    g.Dispose();

    g = Graphics.FromImage(bitmapGreenOpacityX);
    g.Clear(Color.FromArgb(50, 0, 255, 0));
    g.Dispose();

    g = Graphics.FromImage(bitmapGreenOpacityY);
    g.Clear(Color.FromArgb(180, 0, 255, 0));
    g.Dispose();

    //Create hdc's
    IntPtr screenDc = GetDC(IntPtr.Zero);
    hdcMemForm = CreateCompatibleDC(screenDc);
    hdcMemTransparent = CreateCompatibleDC(screenDc);
    hdcMemGreenOpacityX = CreateCompatibleDC(screenDc);
    hdcMemGreenOpacityY = CreateCompatibleDC(screenDc);

    hBitmapForm = bitmapForm.GetHbitmap(Color.FromArgb(0));
    hBitmapFormOld = SelectObject(hdcMemForm, hBitmapForm);

    hBitmapTransparent = bitmapTransparent.GetHbitmap(Color.FromArgb(0));
    hBitmapTransparentOld = SelectObject(hdcMemTransparent, hBitmapTransparent);

    hBitmapGreenOpacityX = bitmapGreenOpacityX.GetHbitmap(Color.FromArgb(0));
    hBitmapGreenOpacityXOld = SelectObject(hdcMemGreenOpacityX, hBitmapGreenOpacityX);

    hBitmapGreenOpacityY = bitmapGreenOpacityY.GetHbitmap(Color.FromArgb(0));
    hBitmapGreenOpacityYOld = SelectObject(hdcMemGreenOpacityY, hBitmapGreenOpacityY);

    DeleteDC(screenDc);

    //copy hdcMemGreenOpacityX to hdcMemForm
    BitBlt(hdcMemForm, 0, 0, 500, 500, hdcMemGreenOpacityX, 0, 0, SRCCOPY);

    blend = new BLENDFUNCTION();

    // Only works with a 32bpp bitmap
    blend.BlendOp = AC_SRC_OVER;
    // Always 0
    blend.BlendFlags = 0;
    // Set to 255 for per-pixel alpha values
    blend.SourceConstantAlpha = 255;
    // Only works when the bitmap contains an alpha channel
    blend.AlphaFormat = AC_SRC_ALPHA;

    newLocation = new POINT(this.Location.X, this.Location.Y);

    //Update the window
    UpdateLayeredWindow(Handle, IntPtr.Zero, ref newLocation, ref newSize, hdcMemForm, ref sourceLocation,
             0, ref blend, ULW_ALPHA);
}

private void Form1_MouseMove(object sender, MouseEventArgs e)
{
    long start;
    long stop;
    long frequency;
     double elapsedTime;

    if (e.Button == System.Windows.Forms.MouseButtons.Left)
    {
        QueryPerformanceFrequency(out frequency);
        QueryPerformanceCounter(out start);

        //copy hdcMemGreenOpacityX to hdcMemForm
        BitBlt(hdcMemForm, 0, 0, 500, 500, hdcMemGreenOpacityX, 0, 0, SRCCOPY);

        //clear the rectangle that we draw with mouse with transparent color
        BitBlt(hdcMemForm, Math.Min(pnt.X, e.X), Math.Min(pnt.Y, e.Y),
                   Math.Abs(pnt.X - e.X), Math.Abs(pnt.Y - e.Y), hdcMemTransparent, 0, 0, SRCCOPY);

        //copy hdcMemGreenOpacityY to hdcMemForm
        BitBlt(hdcMemForm, Math.Min(pnt.X, e.X), Math.Min(pnt.Y, e.Y),
                   Math.Abs(pnt.X - e.X), Math.Abs(pnt.Y - e.Y), hdcMemGreenOpacityY, 0, 0, SRCCOPY);

        newLocation = new POINT(this.Location.X, this.Location.Y);

        //Update the window
        UpdateLayeredWindow(Handle, IntPtr.Zero, ref newLocation, ref newSize, hdcMemForm, ref sourceLocation,
             0, ref blend, ULW_ALPHA);

        QueryPerformanceCounter(out stop);

        elapsedTime = ((double)(stop - start) * 1000000.0d) / (double)frequency;
        Console.WriteLine("{0} μsec", elapsedTime);

        //GDI ~ 1.2 msec!!
    }
}

Release resources:

private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
    if (hBitmapForm != IntPtr.Zero)
    {
        SelectObject(hdcMemForm, hBitmapFormOld);
        DeleteObject(hBitmapForm);
        DeleteDC(hdcMemForm);
        hdcMemForm = IntPtr.Zero;
        hBitmapForm = IntPtr.Zero;
    }

    if (hBitmapTransparent != IntPtr.Zero)
    {
        SelectObject(hdcMemTransparent, hBitmapTransparentOld);
        DeleteObject(hBitmapTransparent);
        DeleteDC(hdcMemTransparent);
        hdcMemTransparent = IntPtr.Zero;
        hBitmapTransparent = IntPtr.Zero;
    }

    if (hBitmapGreenOpacityX != IntPtr.Zero)
    {
        SelectObject(hdcMemGreenOpacityX, hBitmapGreenOpacityXOld);
        DeleteObject(hBitmapGreenOpacityX);
        DeleteDC(hdcMemGreenOpacityX);
        hdcMemGreenOpacityX = IntPtr.Zero;
        hBitmapGreenOpacityX = IntPtr.Zero;
    }

    if (hBitmapGreenOpacityY != IntPtr.Zero)
    {
        SelectObject(hdcMemGreenOpacityY, hBitmapGreenOpacityYOld);
        DeleteObject(hBitmapGreenOpacityY);
        DeleteDC(hdcMemGreenOpacityY);
        hdcMemGreenOpacityY = IntPtr.Zero;
        hBitmapGreenOpacityY = IntPtr.Zero;
    }

    if (bitmapForm != null)
    {
        bitmapForm.Dispose();
        bitmapForm = null;
    }

    if (bitmapTransparent != null)
    {
        bitmapTransparent.Dispose();
        bitmapTransparent = null;
    }

    if (bitmapGreenOpacityX != null)
    {
        bitmapGreenOpacityX.Dispose();
        bitmapGreenOpacityX = null;
    }

    if (bitmapGreenOpacityY != null)
    {
        bitmapGreenOpacityY.Dispose();
        bitmapGreenOpacityY = null;
    }

}

Final results:

GDI+ ~ 12 msec
GDI ~ 1.2 msec ten times faster!

  • This is a very cool idea and I think its the only way to make this work with C# winforms, at least the way I want it to. I decided that doing this with winforms is bad for performance, because the rectangle will be updated in real time, and I went with a visual c++ with standard windows libraries. I wanted to make a snipping tool like gyazo – dimitris93 Apr 24 '15 at 21:49
  • @Shiro Indeed it slower than the c/c++ one **but** you can obtain the same speed(in c#) as in c++ using `hdc's` instead of bitmaps and the `AlphaBelnd` function. This code need minor changes. It is quite easy. I have done it with VB and is extremely fast! – γηράσκω δ' αεί πολλά διδασκόμε Apr 24 '15 at 21:56
  • So instead of `Graphics g = Graphics.FromImage(bmp);`, what did you use ? `Graphics.FromHwnd(IntPtr.Zero);` is flickering and `Graphics.FromHwnd(this.Handle);` doesn't compile – dimitris93 Apr 24 '15 at 22:25