问题描述
我在做什么
我正在开发一个 C#/.NET 4.7.2/WinForms 应用程序,该应用程序使用 Graphics.FillRectangle
.
I am working on a C#/.NET 4.7.2/WinForms app that draws a significant number of filled rectangles on a form using Graphics.FillRectangle
.
目前,矩形是在表单的 Paint
事件中绘制的.绘制完所有矩形后,根据鼠标位置绘制一个十字准线.
Currently, the rectangles are drawn in the form's Paint
event. After all the rectangles are drawn, a crosshair is drawn based on mouse position.
每当鼠标移动时,就会在表单上调用 Invalidate
以强制重新绘制,以便十字准线出现在其新位置.
Whenever the mouse moves, Invalidate
is called on the form to force a repaint so that the crosshair appears in its new position.
问题
这是低效的,因为矩形不会改变,只有十字准线的位置,但矩形每次都被重绘.鼠标移动期间 CPU 使用率很高.
This is inefficient because the rectangles don't change, only the crosshair position, yet the rectangles are being redrawn every time. The CPU usage during mouse move is significant.
下一步
我相信这个问题的解决方案是首先将矩形绘制到缓冲区(在 Paint
事件之外).然后,Paint
事件只需要渲染缓冲区并在顶部绘制一个十字准线.
I believe that the solution to this problem is to draw the rectangles to a buffer first (outside of the Paint
event). Then, the Paint
event only needs to render the buffer plus draw a crosshair on top.
由于我是 GDI+ 和手动缓冲的新手,所以我很担心走错路.Google 搜索显示了大量关于手动缓冲的文章,但每篇文章似乎都采用了不同的方法,这让我更加困惑.
Since I am new to GDI+ and manual buffering, I am wary of going down the wrong track. Google searches reveal plenty of articles on manual buffering, but each article seems to take a different approach which adds to my confusion.
如果您提供有利于简单性和效率的建议方法,我将不胜感激.如果有一种惯用的 .NET 方式来做到这一点——它意味着的方式——我很想知道.
I would be grateful for suggested approaches that favour simplicity and efficiency. If there is an idiomatic .NET way of doing this — the way it's meant to be done — I'd love to know.
推荐答案
这是一个不需要任何缓冲的快速简便的解决方案.要复制这一点,请从一个新的 Windows 窗体项目开始.我只画了两个矩形,但你想画多少就画多少.
Here's a quick and easy solution that doesn't require any buffering. To replicate this, start with a fresh Windows Forms project. I only draw two rectangles, but you can have as many as you want.
如果您使用这两个成员变量和这两个处理程序创建一个新的 WinForms 项目,您将获得一个工作示例.
If you create a new WinForms project with these two member variables and these two handlers, you will get a working sample.
首先,为您的表单添加几个成员变量:
First, a couple of member variables for your form:
private bool _started = false;
private Point _lastPoint;
第一次移动鼠标后,started
标志将变为 true
._lastPoint
字段将跟踪绘制最后一个十字准线的点(这主要是 _started
存在的原因).
The started
flag will turn to true
after the first mouse move. The _lastPoint
field will track the point at which the last cross-hairs was drawn (that's mostly why _started
exists).
Paint
处理程序将在每次调用时绘制十字准线(您将了解为什么使用 MouseMove
处理程序可以这样做):
The Paint
handler will draw the cross hairs every time it's called (you'll see why this is ok with the MouseMove
handler):
private void Form1_Paint(object sender, PaintEventArgs e)
{
var graphics = e.Graphics;
var clientRectangle = this.ClientRectangle;
//draw a couple of rectangles
var firstRectangle = clientRectangle;
firstRectangle.Inflate(-20, -40);
graphics.FillRectangle(Brushes.Aqua, firstRectangle);
var secondRectangle = clientRectangle;
secondRectangle.Inflate(-100, -4);
graphics.FillRectangle(Brushes.Red, secondRectangle);
//draw Cross-Hairs
if (_started)
{
//horizontal
graphics.DrawLine(Pens.LightGray, new Point(clientRectangle.X, _lastPoint.Y),
new Point(ClientRectangle.Width + clientRectangle.X, _lastPoint.Y));
//vertical
graphics.DrawLine(Pens.LightGray, new Point(_lastPoint.X, clientRectangle.Y),
new Point(_lastPoint.X, ClientRectangle.Height + clientRectangle.Y));
}
}
现在是 MouseMove
处理程序.这就是魔法发生的地方.
Now comes the MouseMove
handler. It's where the magic happens.
private void Form1_MouseMove(object sender, MouseEventArgs e)
{
var clientRectangle = this.ClientRectangle;
var position = e.Location;
if (clientRectangle.Contains(position))
{
Rectangle horizontalInvalidationRect;
Rectangle verticalInvalidationRect;
if (_started)
{
horizontalInvalidationRect = new Rectangle(clientRectangle.X,
Math.Max(_lastPoint.Y - 1, clientRectangle.Y), clientRectangle.Width, 3);
verticalInvalidationRect = new Rectangle(Math.Max(_lastPoint.X - 1, clientRectangle.X),
clientRectangle.Y, 3, clientRectangle.Height);
Invalidate(horizontalInvalidationRect);
Invalidate(verticalInvalidationRect);
}
_started = true;
_lastPoint = position;
horizontalInvalidationRect = new Rectangle(clientRectangle.X,
Math.Max(_lastPoint.Y - 1, clientRectangle.Y), clientRectangle.Width, 3);
verticalInvalidationRect = new Rectangle(Math.Max(_lastPoint.X, clientRectangle.X - 1),
clientRectangle.Y, 3, clientRectangle.Height);
Invalidate(horizontalInvalidationRect);
Invalidate(verticalInvalidationRect);
}
}
如果光标在表单内,我会做很多工作.首先,我声明两个矩形,我将用于无效.水平矩形将是一个填充客户端矩形宽度的矩形,但只有 3 个像素高,以我想要无效的区域的 Y 坐标为中心.垂直的矩形与客户端矩形一样高,但只有 3 像素宽.它以我想要无效的区域的 X 坐标为中心.
If the cursor is within the form, I do a bunch of work. First I declare two rectangles that I will be using for invalidate. The horizontal one will be a rectangle that fills the width of the client rectangle, but is only 3 pixels high, centered on the Y coordinate of the area that I want to invalidate. The vertical one is as high as the client rectangle, but only 3 pixels wide. It's centered on the X coordinate of the area that I want to invalidate.
当 Paint 处理程序运行时,它实际上会绘制整个客户区,但实际上只有整个无效区域中的像素被绘制在屏幕上.无效区域之外的任何内容都将被保留.
When the Paint handler runs, it virtually paints the entire client area, but only the pixels in the total invalidated area actually get drawn on the screen. Anything outside the invalidate area is left alone.
因此,当鼠标移动时,我创建了两个矩形(一个垂直,一个水平),围绕最后一组十字准线所在的位置(这样当绘制这些矩形中的像素(包括背景)时,旧的十字准线被有效地擦除),然后我创建了两个新的矩形,围绕当前十字准线的位置(导致背景和新的十字准线被绘制).
So, when the mouse moves, I create two rectangles (one vertical, one horizontal) that surround where the last set of cross-hairs were (so that when the pixels in those rectangles are drawn (including the background), the old cross-hairs are effectively erased) and then I create two new rectangles surrounding where the current cross-hairs should go (causing the background and the new cross-hairs to be drawn).
如果您有一个复杂的绘图应用程序,您将会想要了解失效矩形.例如,当表单被调整大小时,你想要做的只是使新展开的矩形无效,这样就不需要渲染整个绘图.
You are going to want to learn about invalidation rectangles if you have a complicated drawing app. For example, when the form is resized, what you want to do is invalidate only the newly unveiled rectangle(s), so that the whole drawing doesn't need to be rendered.
这可行,但为十字准线选择颜色(或画笔)以使其始终显示可能很困难.使用我的另一个建议(你画线两次(一个擦除,一个绘制)使用 INVERT(即 XOR)画笔更快,并且它总是显示.
This works, but picking a color (or a brush) for the cross-hairs so that they always show can be difficult. Using my other suggestion (that you draw the lines twice (one to erase, one to draw) using an INVERT (i.e. XOR) brush is faster, and it always shows.
这篇关于我应该如何缓冲绘制的矩形以提高性能(C#/.NET/WinForms/GDI+)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!