Delphi 的 TCanvas 是否有错误?

发布于 2024-11-09 03:40:06 字数 2708 浏览 0 评论 0原文

我只是想把它扔在这里以获得一些反馈,我称之为“记得数数零”(感谢 Andreas Rejbrand 提供的链接。事实证明,在处理像素时这被称为“差一问题”)。记得数零是什么意思?好吧,如果您实现一个需要计算矩形操作中涉及的像素数的例程(例如 FillRectCopyRect),您必须记住零 (0,0)是一个像素。但是,将零视为像素而不是无值数字的规则似乎仅在涉及 << 的坐标中起作用。 = 0 个值。举个例子:

mRect:=Rect(0,0,10,10);
mRectWidth:=mRect.right-mRect.left; // returns 10 - 0 = 10

看到问题了吗?从像素操作的角度来看,矩形实际上定义了一个从位置 0,0 延伸到位置 10, 10 的区域。这实际上是 11 个步长,而不是 10 个(对于 x:=0 到 10 实际上是 11 个步长)。为了弥补丢失的像素(零没有质量,当你将其移动到正空间或负空间时就会消失。我似乎记得上帝的毕达哥拉斯定理)大多数人只是在最终结果中添加 1,如下所示:

function getRectWidth(const aRect:TRect):Integer;
Begin
  result:=(aRect.right-aRect.left) +1;
End;

现在这有效,事实上,它的效果非常好,以至于 90% 的图形库都使用它作为计算矩形宽度和高度的技术。但就像强大的英雄 Achillees 一样,它也有一个弱点,即空矩形返回时质量为 1(如果你将它与 blitter 一起使用,它还可以创建各种有趣的 AV)。

mRect:=Rect(0,0,0,0);
mRectWidth:=(mRect.right-mRect.left) + 1;

大致相当于 0 – 0 = 0 : +1 = 1,这意味着如果您不注意盲点,就会渲染一个像素。令我困惑的是,Delphi XE 实际上似乎存在裁剪问题(?),或者至少是术语上的矛盾。因为如果您绘制到底部和最右侧,实际上会丢失一个像素。 ClientRect 不应该返回从第一个像素到最后一个像素的完整绘图范围吗? – 但如果你尝试这样做:

mRect:=getClientRect;
MoveTo(mRect.left,mRect.Bottom);
LineTo(mRect.right,mRect.bottom);

你什么也看不到!因为 Delphi 剪辑了最后的像素(错误地?)。奇怪的是,当您请求 clientrect 时,您必须手动调整它吗?

我从头开始编写了自己的图形库(用于快速 dib 访问和离屏渲染,与这种特殊情况无关),所以我已经在这些方法中工作了很长时间。在编码方面总是有新的东西需要学习,但没有人能告诉我这些材料在工作中没有盲点。

当我将 VCL 的工作方式与其他库(尤其是那些用 C# 编写的库)进行比较时,我还注意到其中很多库都像我一样 - 并确保 clientrect 是您所在区域的完整范围。可以一起工作。当位块传输到剪辑区域之外并使用重叠的矩形时,他们还获取了“盲点”的高度。

盲点的情况

假设您正在将一个矩形从一个位图复制到另一个位图。 blit 的目标是 Rect(-10,-10,10,10)。为了正确地“剪辑”此处的目标,这样您就不会因在内存缓冲区之外写入而发生访问冲突,您必须计算 X1/Y1 和 cliprect 之间的距离(此处取为0,0,Width-1,Height-1)。

这为您提供了必须添加到目标矩形和源矩形的偏移量。否则,您将在缓冲区外写入​​,但也会从源缓冲区中的错误位置读取。

现在,这取决于你如何实现这一点。但是有很多库没有考虑零。当 X1 和 X2 具有相同的值,但 x1 为负时,就会出现盲点。因为人们通常会写:mOffset:= x2 - abs(x1)。在我们的例子中变成 10-10 = 0。只要 cliprect 设置为 0,0 就可以正常工作。但是当您的 cliprect 移动到正空间时 - 您将偏离一个像素。如果您自动添加 getRectWidth 中的值(例如 mWidth:=aRect.right-aRect.left +1) - 您将偏离 2 个像素,具体取决于源矩形(我知道,这是一个很无聊的东西)。

在 Mac 上的 C# 下,使用 GTK# 以及本机 MonoMac 绑定 - clientrect 是绝对的。这意味着您可以绘制到 mRect.bottommRect.right 并获得可见的结果。因此,我发现奇怪的是,我最喜欢的语言和工具包 Delphi,当我们使用它时,我们总是必须手动调整每个 ownerdrawn 或自定义控件的 clientrect

I'm just going to throw this out here to get some feedback on it, what I call "remember to count zero" (thanks Andreas Rejbrand for the link. It turned out it's called the "off by one problem") when working with pixels. What do I mean by remember to count zero? Well, if you implement a routine that needs to calculate the number of pixels involved in a rectangular operation (e.g FillRect or CopyRect) you must remember that zero (0,0) is a pixel to. But the rule of regarding zero as a pixel rather than a number of no value, only seem to come into play with coordinates involving < = 0 values. Take this example:

mRect:=Rect(0,0,10,10);
mRectWidth:=mRect.right-mRect.left; // returns 10 - 0 = 10

See the problem? the rectangle actually defines, in pixel-operation terms, a region stretching from position 0,0 to position 10, 10. Which is actually 11 steps long, not ten (for x:=0 to 10 is actually 11 steps). To make up for the lost pixel (zero has no mass and vanish when you move it into positive or negative space. The Pythagorean theorem of God I seem to remember) most people just add 1 to the final result, like this:

function getRectWidth(const aRect:TRect):Integer;
Begin
  result:=(aRect.right-aRect.left) +1;
End;

Now this works, in fact it works so well that 90% of all graphics libraries use this as their technique to calculate the width and height of a rectangle. But just like the mighty hero Achillees it has a weak spot, namely that empty rectangles return as having the mass of 1 (It can also create all sorts of funny AV's if you use it with a blitter).

mRect:=Rect(0,0,0,0);
mRectWidth:=(mRect.right-mRect.left) + 1;

Which roughly equates to 0 – 0 = 0 : +1 = 1, which means that a pixel will be rendered if you don't look out for the blind-spot. What puzzles me is that, Delphi XE actually seems to have a clipping problem (?), or at least a contradiction in terms. Because you actually lose one pixel at the bottom and to the utmost right if you draw to it. Shouldn't ClientRect return the full drawing scope from the first pixel to the last? – yet if you try this:

mRect:=getClientRect;
MoveTo(mRect.left,mRect.Bottom);
LineTo(mRect.right,mRect.bottom);

You won't see a thing! Because Delphi clips the final pixel (by mistake?). It just seems curious that when you ask for the clientrect, that you manually have to adjust it?

I have coded my own graphics libraries from scratch (for fast dib access and offscreen rendering, nothing to do with this particular case), so I have worked inside these methods for a long time now. There is always something new to learn when it comes to coding, but no one can tell me that there isn't a blind spot at work in this material.

When I compared how the VCL does things to other libraries, especially those written in C# I also noticed that a lot of them did like me - and made sure that a clientrect IS the full scope of the region you can work with. And they also took height for the "blind spot" when blitting outside the clipregion and working with overlapping rectangles.

The case of the blindspot

Let us say you are copying a rectangle from one bitmap to the other. The target for your blit is Rect(-10,-10,10,10). In order to correctly "clip" the target here, so you don't get an access violation for writing outside your memory buffer, you have to calculate the distance between X1/Y1 and your cliprect (here taken to be 0,0,Width-1,Height-1).

This gives you an offset that must be added to the target rectangle and the source rectangle. Otherwise you will write outside the buffer but also read from the wrong place in the source buffer.

Now, it depends on how you implement this off course. But there are plenty of libraries out there that don't take zero into account. The blind-spot occurs when X1 and X2 has the same value, but x1 is negative. Because people usually write: mOffset:= x2 - abs(x1). Which in our case becomes 10-10 = 0. And as long as the cliprect is set to 0,0 it will work just fine. But the moment your cliprect moves into positive space - you will be off by one pixel. And if you automatically Inc the values in your getRectWidth (e.g mWidth:=aRect.right-aRect.left +1) - you will be off by 2 pixels depending on the source rectangle (I know, this is major boring stuff).

Under C# on the Mac, using GTK# and also the native MonoMac bindings - the clientrect is absolute. Meaning that you can draw to mRect.bottom or mRect.right and have visible results. Hence I found it curious that my favorite language and toolkit, Delphi, we always have to do manually adjust the clientrect of every ownerdrawn or custom control when we work with it.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

半衾梦 2024-11-16 03:40:06

这就是 GDI 的工作原理,而 Delphi 的 TCanvas 仅反映了底层框架。

例如,考虑 LineTo() :

LineTo 函数从当前位置到指定点(但不包括指定点)绘制一条线。

或者FillRect()

FillRect函数使用指定的画笔填充矩形。此函数包括矩形的左边框和上边框,但不包括右边框和下边框。

或者矩形()

绘制的矩形不包括底部和右边缘。

等等等等。

现在考虑 API 函数 GetWindowRect()

检索指定窗口的边界矩形的尺寸。尺寸以相对于屏幕左上角的屏幕坐标给出。

返回的 RECT 中的 bottomright 值超出窗口边界 1 个像素。所以窗口的宽度实际上是width = right-left,高度也是如此。我猜测,选择这个约定是为了保持这种平等。

您报告的行为不是 Delphi 的 TCanvas 代码中的错误 - 该代码工作正常并且完全按照设计。

到目前为止,对于使用 Windows UI 的开发人员来说,最好的方法是遵循相同的约定。尝试采用自己的不同约定只会导致混乱和错误。

This is how GDI works and Delphi's TCanvas merely mirrors the underlying framework.

For example, consider LineTo():

The LineTo function draws a line from the current position up to, but not including, the specified point.

Or FillRect():

The FillRect function fills a rectangle by using the specified brush. This function includes the left and top borders, but excludes the right and bottom borders of the rectangle.

Or Rectangle():

The rectangle that is drawn excludes the bottom and right edges.

And so on and so on.

Consider now the API function GetWindowRect().

Retrieves the dimensions of the bounding rectangle of the specified window. The dimensions are given in screen coordinates that are relative to the upper-left corner of the screen.

The bottom and right values in the returned RECT are 1 pixel beyond the boundary of the window. So the width of the window really is width = right-left and likewise for the height. It is my guess that the convention was chosen so that this equality holds.

The behaviour you report is not a bug in Delphi's TCanvas code — the code works correctly and exactly as designed.

By far the best approach for developers working with Windows UI is to follow the same conventions. Attempting to adopt your own different conventions will simply lead to confusion and bugs.

三人与歌 2024-11-16 03:40:06

我对您为这种现象编写完整库的努力感到非常抱歉,但您完全错了。

例如,您的代码应如下所示:

mRect := getClientRect;
MoveTo(mRect.left, mRect.Bottom - 1);
LineTo(mRect.right - 1, mRect.bottom - 1);

始终考虑到像 FillRect() 这样的例程不会对 X = Rect.RightY = Rect.Bottom 执行任何操作。它们都绘制到Right - 1Bottom - 1。事情应该是这样的:对于 Left = 10Width = 10 的按钮,最右边的像素位于 X = 19 处,而不是X = 20

也许这很令人困惑,但你可以很容易地将其形象化在纸质方块上。

I am terribly sorry for your effort writing a complete library for this phenomenon, but you are totally wrong.

For example, your code should be like:

mRect := getClientRect;
MoveTo(mRect.left, mRect.Bottom - 1);
LineTo(mRect.right - 1, mRect.bottom - 1);

Always take into account that routines like FillRect() do nothing on X = Rect.Right nor Y = Rect.Bottom. They all draw until Right - 1 and Bottom - 1. That's how it should be: for a button with Left = 10 and Width = 10, de rightmost pixel is found at X = 19, not X = 20.

Maybe it ís confusing, but you can easily visualize this on a paper square block.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文