使用CreateGraphics而不是Paint事件处理程序来绘制自定义绘图时的毛刺

我写了一个Windows窗体应用程序,我使用Control.CreateGraphics()Panel上自定义绘图。 这是我的Form在启动时的样子:

空白的形式与绘制!按键

自定义绘图是在“绘制!”的Click事件处理程序的顶部面板上执行的。 button。 这是我的button点击处理程序:

 private void drawButton_Click(object sender, EventArgs e) { using (Graphics g = drawPanel.CreateGraphics()) { g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; g.Clear(Color.White); Size size = drawPanel.ClientSize; Rectangle bounds = drawPanel.ClientRectangle; bounds.Inflate(-10, -10); g.FillEllipse(Brushes.LightGreen, bounds); g.DrawEllipse(Pens.Black, bounds); } } 

点击drawButton ,表单看起来像这样:

用绿色填充的椭圆形式

成功!

但是当我通过拖动一个angular来缩小表格的时候…

同样的形式,收缩以部分隐藏椭圆

…并将其扩大回原来的大小,

在右侧和底部形成缺失的图形

我画的一部分没了!

当我将部分窗口拖离屏幕时,也会发生这种情况。

形成部分屏幕外

…并将其拖回屏幕上:

形成在屏幕上,但有一半的图形丢失

如果我最小化窗口并恢复它,整个图像被删除:

空白表单像第一个图像

这是什么原因造成的? 我怎样才能做到这一点,所以我画的graphics是持久的?

注:我已经创build了这个自我回答的问题,所以我有一个规范的Q / A来引导用户,因为如果你不知道问题的原因,这是一个很难search的常见情况。

TL; DR:

不要使用Control.CreateGraphics来响应一次性的UI事件。 而是为要绘制的控件注册Paint事件处理程序,并使用通过PaintEventArgs传递的Graphics对象进行绘制。

如果只想在点击button(例如)之后绘制,请在Click处理程序中设置一个布尔型标志,指示该button已被单击,然后调用Control.Invalidate() 。 然后在Paint处理程序中有条件地执行渲染。

最后,如果你的控件的内容应该随着控件的大小而改变,那么注册一个Resize事件处理器并且调用Invalidate()。

示例代码:

 private bool _doCustomDrawing = false; private void drawPanel_Paint(object sender, PaintEventArgs e) { if (_doCustomDrawing) { Graphics g = e.Graphics; g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; g.Clear(Color.White); Size size = drawPanel.ClientSize; Rectangle bounds = drawPanel.ClientRectangle; bounds.Inflate(-10, -10); g.FillEllipse(Brushes.LightGreen, bounds); g.DrawEllipse(Pens.Black, bounds); } } private void drawButton_Click(object sender, EventArgs e) { _doCustomDrawing = true; drawPanel.Invalidate(); } private void drawPanel_Resize(object sender, EventArgs e) { drawPanel.Invalidate(); } 

但为什么? 我做错了什么,这是怎么解决的呢?

看看Control.CreateGraphics的文档 :

通过CreateGraphics方法检索的graphics对象通常不应当在当前的Windows消息处理后保留,因为用该对象绘制的任何内容都将被删除,并显示下一条WM_PAINT消息。

Windows不负责保留您绘制到您的Control的graphics。 而是确定您的控件需要重绘的情况,并用WM_PAINT消息通知它。 然后,您可以自行重新绘制。 这发生在OnPaint方法中,如果您inheritance了Control或其子类的子类,则可以覆盖该方法。 如果您不是inheritance类,则仍然可以通过处理公共Paint事件来执行自定义绘图,该事件在控件的OnPaint方法结束时触发。 这是你想要挂钩的地方,以确保每次重新绘制Control重新绘制graphics。 否则,部分或全部控件将被绘制到控件的默认外观上。

全部或部分控件失效时重新进行重绘 。 通过调用Control.Invalidate() ,可以使整个控件失效,请求完全重绘。 其他情况可能只需要部分重绘。 如果Windows确定只有部分Control需要重新绘制,则您收到的PaintEventArgs将具有非空的ClipRegion 。 在这种情况下,即使您尝试绘制区域以外的区域,您的绘图也只会影响ClipRegion中的区域。 这就是为什么在上面的例子中需要调用drawPanel.Invalidate() 。 由于drawPanel的外观需要随着控件的大小而改变,并且在窗口展开时只有控件的新部分是无效的,所以有必要请求每个resize的完整重绘。