Is there a bug in Delphi's TCanvas?
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 t开发者_Go百科he 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.
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.
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.
精彩评论