自定义控件的本质
自定义控件的本质只有两点:
- 重绘控件Region区域(圆角、多边形、图片等),这是整个控件的真实范围。缺点是Region无法抗锯齿,自定义的Region范围是有锯齿的,无法消除;此外新的Region还会和绘制的背景产生1像素的白边(在圆角或图形拐角部分),且几乎无法有效的消除。【后续会介绍FillRegion填充区域,而不是绘制图形与Region产生'白边'】
- 重绘图形,在原有Region【矩形区域】范围内,重绘不同的图形(圆角、多边形、图片等)作为背景,因为绘制图形时可以实现抗锯齿,因此看起来像无锯齿控件一样。本质上是在固定Region内绘制的尽可能大的无锯齿图形,因此需要注意设置透明背景(透明自定义图形外的Region部分)。
如果再加一条的话,自定义控件还有一点,就是:
- 在原有控件上通过设置图片/图标、背景颜色、布局大小、多个不同控件组合等,同时结合重绘图形(或重绘Region区域),实现一定的自定义。
在其实现方法上,都是使用GDI/GDI+进行绘制,并添加一些默认的功能、事件或属性;组合控件也可能不需要使用GDI+绘制;还有一些基于基础控件的扩展控件,在原控件的基础上进行一些扩展,也用不到绘制方法。
赋值新Region和直接重绘图形的区别
无论是在控件的Paint事件方法中,还是通过继承控件在OnPaint方法中,重绘控件的两种方式:赋值新Region和直接重绘图形,在进行绘制时都有两个相同的区别。
- 仅仅赋值新Region原则上不需要再重新绘制背景颜色、文本等;
- 只要发生了绘制(无论直接在原Region上绘制图形、还是赋值了新Region后绘制图形),都需要再重新绘制文本。即绘制图形会覆盖原来的内容,需要背景、文本都进行绘制
不要使用e.ClipRectangle
、不要直接修改Region
无论是在继承控件的OnPaint方法中,还是在控件的Paint事件方法中,都不要使用参数e.ClipRectangle
作为控件重绘的区域(绘制图形或重新创建Region),原因之前文章已经介绍。
另外,对于赋值新Region,不要直接在代码中实现,原因和上面一样,在发生控件大小、位置、拖动等变化时,会发生显示错乱或不完全的部分(直接赋值Region则固定了Region区域),正确的做法还是要在Paint事件或OnPaint方法中。
Region区域无法反锯齿
没有最佳圆角最佳自定义控件的实现,除非可以创建无锯齿Region。
无法对Region反锯齿
【目前来说,自定义控件时新建Region没有最优雅或最优实现】,任何创建新Region的方法都是有锯齿的【个人所知🥲】。
在原有Region上绘制没有锯齿的原因,是因为绘制的圆角图形没锯齿,而默认的Region是个大于圆角矩形的直角矩形(直角没锯齿),但Region的直角相对于圆角图形多出来的部分是透明的"看不到",只看到了绘制出来的无锯齿的圆角矩形,形成了"无锯齿的"控件效果(实际是无锯齿的图形)。
以圆角为例,通过GraphicsPath
创建的圆角路径而创建的Region区域,在仅赋值新Region的情况下,可以免去绘制Graphics的圆角图形和文字的麻烦,但是会有锯齿问题。
比如,之前介绍的创建新Region:
Rectangle controlRect = new Rectangle(0, 0, this.Width, this.Height);
var controlPath = controlRect.GetRoundedRectPath(roundRadius);
// 要在绘制之前指定Region,否则无效
this.Region = new Region(controlPath);
GDI+的APICreateRoundRectRgn
GDI+ API 的 CreateRoundRectRgn
方法是另外一种创建Region的方式(Region.FromHrgn()
方法),但是其仍然无法做到抗锯齿。
[DllImport("Gdi32.dll", EntryPoint = "CreateRoundRectRgn")]
private static extern IntPtr CreateRoundRectRgn
(
int nLeftRect, // x-coordinate of upper-left corner
int nTopRect, // y-coordinate of upper-left corner
int nRightRect, // x-coordinate of lower-right corner
int nBottomRect, // y-coordinate of lower-right corner
int nWidthEllipse, // height of ellipse
int nHeightEllipse // width of ellipse
);
// ...
// 创建Region
Region = Region.FromHrgn(CreateRoundRectRgn(0, 0, this.Width, this.Height, roundRadius, roundRadius)); //同样无法抗锯齿
Paint事件中使用CreateRoundRectRgn API的示例
// button1 需处理为无边框
button1.FlatStyle = FlatStyle.Flat;
button1.FlatAppearance.BorderSize = 0;
button1.FlatAppearance.CheckedBackColor = Color.Transparent;
button1.Paint += Button_Paint;
panel1.Paint += Panel_Paint; ;
label1.Paint += Label1_Paint;
// ........
private void Label1_Paint(object sender, PaintEventArgs e)
{
var pnl = (Label)sender;
pnl.Region = Region.FromHrgn(CreateRoundRectRgn(0, 0, pnl.Width, pnl.Height, 30, 30));
}
private void Panel_Paint(object sender, PaintEventArgs e)
{
var pnl = (Panel)sender;
pnl.Region = Region.FromHrgn(CreateRoundRectRgn(0, 0, pnl.Width, pnl.Height, 30, 30));
}
private void Button_Paint(object sender, PaintEventArgs e)
{
var btn = (Button)sender;
btn.Region = Region.FromHrgn(CreateRoundRectRgn(0, 0, btn.Width, btn.Height, 30, 30));
}
效果如下,可以看到有着响应的锯齿。和直接使用托管代码new Region(roundedPath)
创建圆角Region是一样的。
Graphics.FillRegion 填充Region同样有锯齿
后面看到Graphics.FillRegion
方法,填充Region也是一样,依旧会有锯齿和圆角边缘的白边问题。
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
g.SmoothingMode = SmoothingMode.AntiAlias; // SmoothingMode.HighQuality
g.CompositingQuality = CompositingQuality.HighQuality;
g.InterpolationMode = InterpolationMode.HighQualityBilinear;
var brush = new SolidBrush(bgcolor);
g.FillRegion(brush,region);
关于自定义控件的代码注入或代码共同【如何实现?】
可以发现,对于圆角控件(或自定义)部分的代码,几乎对所有控件处理都是一致。则这样的情况下,针对每个控件都处理添加相同的代码,会显得非常冗余,且如果需要修改,就修改每一个相同的内容。
因此,考虑是否有办法直接将自定义控件的代码通过某种方法注入到控件中,而外部只维护一套自定义的代码【写好测试,任何修改都要考虑是否影响到所有控件】。
如何实现直接注入圆角实现的代码到OnPaint方法中?避免继承控件时重复重写的代码? 待后续研究实现。