C# window form drawing over other controls / 在 window form 中繪製圖形並疊在控制項上

本文以中英夾雜並陳

This article is interleaved with Chinese and English.

本來還想在網路上看能不能找到答案的, 結果只有找到一些可能的提示... 不過最後還是摸索出來了啦 😀

At the beginning, I hope to scrounge the solution from Internet, but after hours of searching, I only find some possible hints. Anyway, here's the solution.

之所以會想要弄這樣的功能,是因為要做出類似在檔案總管中用拖曳選取時有半透明選取區域的效果。但是如果在 window form 中匯製矩形時,會發現這個矩形區域會被其他的控制項壓在下面,使得這個半透明的效果變得很奇怪(如下圖).

This issue is caused by I wanna make a semi-transparent selection area in a window form, just what we usually use in windows file manager. However, if we just draw a rectangle in a window form, this rectangle will be "overlayed" by other controls, just like the figure below. Yep, quite weird.

underlay

而我想要的效果是如下圖這樣的, 半透明的選取區域壓在控制項上方.

The figure below is what I really want is a semi-transparent selection region overlays other controls.
Overlay

最開始的時候我的想法是做一個透明的 Panel, 同時給定顏色,然後隨著滑鼠拖曳動態地去調整大小。結果我在網路上找到了製作透明 Panel 的方法(程式碼如下),但,很遺憾地,對這個透明 Panel 指定顏色還不能達到我想要的效果。

My first trial is creating a transparent Panel and assign some color onto in, and adjust the size as mouse dragging and moving. I scrounged a transparent panel solution from Internet (source code as below), but, unfortunately, assigning color onto it still does not meet what I want.

public class TransparentPanel : System.Windows.Forms.Panel
{
    public TransparentPanel()
    {
    }

    protected override System.Windows.Forms.CreateParams CreateParams
    {
        get
        {
            System.Windows.Forms.CreateParams createParams = base.CreateParams;
            createParams.ExStyle |= 0x00000020; // WS_EX_TRANSPARENT

            return createParams;
        }
    }

    protected override void OnPaintBackground(System.Windows.Forms.PaintEventArgs e)
    {
        // Do not paint background.
    }
}

那, 在這個透明的 panel 上頭畫一個半透明的矩形呢? 似乎有點可行, 但是在滑鼠拖曳移動時會變得很糟糕, 留下許多殘像.

Well, how about drawing a semi-transparent rectangle on the transparent panel? Sounds reasonable, but there are lots of afterimages while mouse dragging and moving.

//draw a semi-transparent rectangle by adjusting the alpha value of Color.FromArgb function.
Rectangle rec = new Rectangle(topLeftRec, howBig);
Graphics.FillRectangle(new SolidBrush(Color.FromArgb(150, 255, 255, 0)), rec);

後來看到一些提示之後找到了解法,必須透過控制項的 Paint event 來把這個矩形匯製出來。雖然還不是很清楚為什麼要這樣做,但感覺起來是把這個矩形的匯製的動作跟著控制項匯製的動作綁在一起。藉由改變 transparent panel 的大小與位置來觸發 Paint event.

Finally, with some hints on Internet, I find the solution -- painting the rectangle as a control's paint event is triggered. Although I still do not fully understand the reason why I have to follow this process, I think it might require combining drawing rectangle with the control's paint process. By changing the size and location of the transparent panel, the Paint event can be triggered.

private void pTransparent_Paint(object sender, PaintEventArgs e)
{
    base.OnPaint(e);
    Graphics dc = e.Graphics;
    
    Pen blackPen = new Pen(Color.Black, 1);
    Point topLeftRec = new Point(e.ClipRectangle.X, e.ClipRectangle.Y);
    Size howBig = new Size(e.ClipRectangle.Width, e.ClipRectangle.Height);
    Rectangle rec = new Rectangle(topLeftRec, howBig);

    Console.WriteLine(topLeftRec.ToString());

    dc.DrawRectangle(blackPen, rec);
    dc.FillRectangle(new SolidBrush(Color.FromArgb(150, 255, 255, 0)), rec);
    blackPen.Dispose();
}

private Rectangle drawrect;
private bool md = false;
private Point startpoint, drawtopleft;
private Size drawsize;

private void flowLayoutPanel1_MouseMove(object sender, MouseEventArgs e)
{
    if (md == true)  //md is a boolean variable representing in mouse dragging mode or not.
    {
        drawtopleft.X = Math.Min(startpoint.X, e.X);
        drawtopleft.Y = Math.Min(startpoint.Y, e.Y);

        drawsize.Width = Math.Abs(startpoint.X - e.X);
        drawsize.Height = Math.Abs(startpoint.Y - e.Y);

        drawrect = new Rectangle(drawtopleft, drawsize);
        ((FlowLayoutPanel)sender).Refresh();

        pTransparent.Width = drawrect.Width;
        pTransparent.Height = drawrect.Height;
        pTransparent.Left = drawtopleft.X;
        pTransparent.Top = drawtopleft.Y;
        }
    }
}