双缓冲为什么应该这样用?

双缓冲绘图机制详解

看了很多的使用双缓冲的例子。有的时候也知道是应该那样用,原理也知道一点,但就是写不出来,为何?

脑子里面没正确的概念,有的只是个皮毛,现在虽然不完全明白。仅就把所知道的写下来,与诸君一同学习。

先上代码:

void CXXXWnd::OnPaint()
{
	CPaintDC dc(this);

	CDC memDC;
	memDC.CreateCompatibleDC(&dc);

	CRect rect;
	GetClientRect(rect);

	CBitmap bm;
	bm.CreateCompatibleBitmap(&dc, rect.Width(), rect.Height());

	memDC.SelectObject(&bm);
	memDC.SetBkMode(TRANSPARENT);

	memDC.FillSolidRect(rect, RGB(128,128,0));

	dc.BitBlt(0,0,rect.Width(),rect.Height(), &memDC, 0, 0, SRCCOPY);
}


双缓冲其实现在内存画图,那么我们需要一个内存DC。

这里定义为CDC memDC,并创建兼容DC。

同理也创建一个兼容源DC的CBitmap。

在内存DC画图,将该DC内容画到实际DC上。


借鉴MSDN双缓冲步骤:

Remarks

A memory device context is a device context that exists only in memory. When the memory device context is created, its display surface is exactly one monochrome pixel wide and one monochrome pixel high.

Before an application can use a memory device context for drawing operations, it must select a bitmap of the correct width and height into the device context. This may be done by using CreateCompatibleBitmap to specify the height, width, and color organization required in the function call.

When a memory device context is created, all attributes are set to typical default values. The memory device context can be use as a typical device context. You can set the attributes to non-default values, obtain the current setting of its attributes, and select pens, brushes and regions into it.

The CreateCompatibleDC function can only be used with devices that support raster operations. An application can determine whether a device supports these operations by calling the GetDeviceCaps function.

When you no longer need the memory device context, call the DeleteDC function to delete it. 



<think>我们已经在之前的回答中介绍了三种启用双缓冲的方法,但用户反馈开启双缓冲后仍然有闪烁。这里需要分析可能的原因并提供解决方案。 可能的原因及解决方法如下: 1. **未正确设置所有必需的ControlStyles标志**: 仅设置`OptimizedDoubleBuffer`是不够的,必须同时设置以下三个标志: ```csharp this.SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint, true); ``` 注意:`UserPaint`允许控件自绘制,`AllPaintingInWmPaint`告诉控件忽略擦除背景消息(由WM_PAINT消息处理所有绘制),这是双缓冲工作所必需的。 2. **在窗体级别启用双缓冲时,子控件未启用双缓冲**: 如果窗体本身启用了双缓冲,但窗体上的控件(如Panel、DataGridView等)没有启用,那么这些控件的绘制仍可能导致闪烁。 解决方法:为每个需要双缓冲的控件单独启用双缓冲(使用反射或继承重写)。 3. **控件本身不支持双缓冲**: 一些标准控件(如DataGridView、ListView)默认不支持双缓冲,即使设置了`DoubleBuffered`属性也可能无效。对于这些控件,我们需要通过反射强制设置其`DoubleBuffered`属性为true(如引用[2]所示)。 4. **在绘制事件中执行了耗时操作**: 在`OnPaint`或`Paint`事件中执行复杂的计算或频繁的绘图操作,可能导致界面卡顿或闪烁。 解决方法:优化绘制代码,避免在绘制事件中进行耗时操作。可以考虑使用后台线程预处理数据,但注意跨线程操作控件的问题。 5. **未正确处理背景绘制**: 当控件背景被擦除时,可能会引起闪烁。确保设置了`ControlStyles.AllPaintingInWmPaint`标志,这样控件将不会在收到WM_ERASEBKGND消息时擦除背景,而是统一在WM_PAINT消息中处理。 6. **重绘区域过大或过于频繁**: 如果每次重绘整个控件,而实际上只需要重绘一小部分,会导致性能下降。可以通过`ClipRectangle`来只绘制需要更新的区域。 另外,使用`Invalidate(Rectangle)`代替无参数的`Invalidate()`来指定需要重绘的区域。 7. **使用WS_EX_COMPOSITED扩展样式(窗体级别双缓冲)**: 在窗体级别启用双缓冲,可以同时缓冲窗体及其所有子控件。但注意,这可能会增加内存开销,并且在某些系统上可能不支持。 ```csharp protected override CreateParams CreateParams { get { CreateParams cp = base.CreateParams; cp.ExStyle |= 0x02000000; // WS_EX_COMPOSITED return cp; } } ``` 8. **资源泄漏**: 如果在绘制过程中创建了画笔、画刷等GDI对象而没有释放,可能导致资源耗尽和闪烁。确保使用`using`语句或手动调用`Dispose()`。 9. **控件重叠**: 当控件重叠时,重绘一个控件可能导致其覆盖的控件也被重绘,从而引起闪烁。可以尝试在容器控件上设置`ControlStyles.ContainerControl`标志,但更有效的方法是使用双缓冲的容器。 10. **其他可能的原因**: - 显卡驱动问题:更新显卡驱动。 - 系统主题:尝试更换系统主题或禁用视觉样式。 - .NET Framework版本:某些旧版本可能存在已知问题,升级到最新版本。 示例:为DataGridView启用双缓冲(解决闪烁) ```csharp public static class ControlHelper { public static void EnableDoubleBuffering(Control control) { // 使用反射设置DoubleBuffered属性 typeof(Control).GetProperty("DoubleBuffered", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) .SetValue(control, true, null); } } // 在窗体构造函数中使用 public Form1() { InitializeComponent(); ControlHelper.EnableDoubleBuffering(dataGridView1); } ``` 如果以上方法都不能解决,可以考虑使用手动双缓冲(如之前的方法三),即创建一个内存中的位图,先绘制到位图上,再一次性绘制到屏幕。 最后,注意在控件大小改变时(Resize事件)重新创建缓冲位图,并处理Dispose释放资源。 总结:双缓冲后仍然闪烁的原因多种多样,需要根据具体情况排查。通常,正确设置ControlStyles三件套、为子控件启用双缓冲、避免耗时绘制操作是关键。 §§ 相关问题 §§ 1. 如何为TreeView控件启用双缓冲? 2. 在C#中如何只重绘控件的部分区域? 3. 双缓冲技术会占用多少内存? 4. 如何检测C#应用程序中的GDI资源泄漏?</think>### C# 双缓冲后仍闪烁的原因与解决方案 启用双缓冲后仍然出现闪烁通常由以下原因导致,以下是详细分析和解决方法: --- #### 🔍 常见原因及解决方案 1. **样式标志设置不完整**(最常见) ```csharp // 错误:缺少关键标志 this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true); // 正确:必须同时设置三个核心标志 this.SetStyle( ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint, true ); ``` - **`AllPaintingInWmPaint`**:阻止系统发送擦除背景消息 - **`UserPaint`**:启用自定义绘制 - 缺少任意一个都会导致闪烁[^1] 2. **子控件未启用双缓冲** ```csharp // 为所有子控件启用双缓冲 public void EnableChildDoubleBuffering(Control parent) { foreach (Control ctrl in parent.Controls) { // 使用反射启用 typeof(Control).GetProperty("DoubleBuffered", BindingFlags.NonPublic | BindingFlags.Instance) .SetValue(ctrl, true, null); // 递归处理嵌套控件 if (ctrl.HasChildren) EnableChildDoubleBuffering(ctrl); } } ``` - 窗体级双缓冲不会自动应用于子控件 - 特别影响 `Panel`, `DataGridView`, `ListView` 等容器控件[^2] 3. **绘制代码效率低下** ```csharp protected override void OnPaint(PaintEventArgs e) { // 避免在绘制事件中创建新对象 using (var brush = new SolidBrush(BackColor)) { e.Graphics.FillRectangle(brush, ClientRectangle); } // 使用预定义的笔刷和字体 e.Graphics.DrawString("Text", SystemFonts.DefaultFont, Brushes.Black, 10, 10); } ``` - 避免在 `OnPaint` 中:创建 `Brush`/`Pen`/`Font` 对象 - 避免复杂计算:将耗时操作移至后台线程 4. **背景擦除未禁用** ```csharp protected override void OnPaintBackground(PaintEventArgs e) { // 空实现阻止默认背景绘制 // base.OnPaintBackground(e); // 注释掉这行 } ``` - 与 `ControlStyles.AllPaintingInWmPaint` 配合使用 - 阻止系统自动擦除背景导致的闪烁 --- #### ⚙ 高级解决方案 1. **窗体级组合双缓冲** ```csharp protected override CreateParams CreateParams { get { CreateParams cp = base.CreateParams; cp.ExStyle |= 0x02000000; // WS_EX_COMPOSITED return cp; } } ``` - 为整个窗体及其子控件启用双缓冲 - 注意:可能增加 10-15% 内存占用[^1] 2. **批量更新优化** ```csharp void UpdateData() { // 暂停重绘 NativeMethods.SendMessage(this.Handle, 0x000B, 0, 0); // WM_SETREDRAW false try { // 批量更新操作... dataGridView1.Rows.Add(...); dataGridView1.Rows.Remove(...); } finally { // 恢复重绘并刷新 NativeMethods.SendMessage(this.Handle, 0x000B, 1, 0); // WM_SETREDRAW true dataGridView1.Refresh(); } } ``` 3. **手动双缓冲实现** ```csharp private Bitmap _buffer; protected override void OnPaint(PaintEventArgs e) { if (_buffer == null || _buffer.Size != ClientSize) _buffer = new Bitmap(ClientSize.Width, ClientSize.Height); using (var g = Graphics.FromImage(_buffer)) { // 1. 在内存中绘制 g.Clear(BackColor); g.DrawEllipse(Pens.Red, 10, 10, 50, 50); // 2. 复制到屏幕 e.Graphics.DrawImage(_buffer, 0, 0); } } ``` - 适用于需要精细控制的场景 --- #### 📌 关键检查清单 | 问题类型 | 检查点 | 验证方法 | |---------|-------|---------| | **样式标志** | 三个核心标志是否齐全 | 检查 `SetStyle` 调用 | | **子控件** | 所有容器控件是否启用双缓冲 | 使用反射检查 `DoubleBuffered` 属性 | | **绘制效率** | `OnPaint` 中是否有耗时操作 | 性能分析工具检测 | | **背景绘制** | `OnPaintBackground` 是否被禁用 | 注释掉基类方法调用 | | **更新频率** | 是否批量更新界面 | 使用 `SuspendLayout`/`ResumeLayout` | > **性能提示**:使用任务管理器监控 GDI 对象计数,若持续增长表明存在资源泄漏(需检查 `Dispose` 调用)[^3]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值