When you build a simple application, a single form might hold all of the components you need. As the application grows, however, you may need to squeeze in the components, increase the size of the form, or add new forms. If you reduce the space occupied by the components, you might add some capability to resize them at run time, possibly splitting the form into different areas. If you choose to increase the size of the form, you might use scroll bars to let the user move around in a form that is bigger than the screen (or at least bigger than its visible portion on the screen).
Adding a scroll bar to a form is simple. In fact, you don't need to do anything. If you place several components in a big form and then reduce its size, a scroll bar will be added to the form automatically, as long as you haven't changed the value of the AutoScroll property from its default of True.
Along with AutoScroll, forms have two properties, HorzScrollBar and VertScrollBar, which can be used to set several properties of the two TFormScrollBar objects associated with the form. The Visible property indicates whether the scroll bar is present, the Position property determines the initial status of the scroll thumb, and the Increment property determines the effect of clicking one of the arrows at the ends of the scroll bar. The most important property, however, is Range.
The Range property of a scroll bar determines the virtual size of the form, not the actual range of values of the scroll bar. Suppose you need a form that will host several components and will therefore need to be 1000 pixels wide. We can use this value to set the "virtual range" of the form, changing the Range of the horizontal scroll bar.
The Position property of the scroll bar will range from 0 to 1000 minus the current size of the client area. For example, if the client area of the form is 300 pixels wide, you can scroll 700 pixels to see the far end of the form (the thousandth pixel).
To demonstrate the specific case I've just discussed, I've built the Scroll1 example, which has a virtual form 1000 pixels wide. To accomplish this, I've set the range of the horizontal scroll bar to 1000:
object Form1: TForm1 Width = 458 Height = 368
HorzScrollBar.Range = 1000 VertScrollBar.Range = 305 AutoScroll = False Caption = 'Scrolling Form' OnResize = FormResize
The form of this example has been filled with meaningless list boxes, and I could have obtained the same scroll-bar range by placing the right-most list box so that its position (Left) plus its size (Width) would equal 1000.
The interesting part of the example is the presence of a toolbox window displaying the status of the form and of its horizontal scroll bar. This second form has four labels; two with fixed text and two with the actual output. Besides this, the secondary form (called Status) has a bsToolWindow border style and is a top-most window. You should also set its Visible property to True, to have its window automatically displayed at startup:
object Status: TStatus
BorderIcons = [biSystemMenu] BorderStyle = bsToolWindow Caption = 'Status' FormStyle = fsStayOnTop Visible = True object Labell: TLabel...
There isn't much code in this program. Its aim is to update the values in the toolbox each time the form is resized or scrolled (as you can see in Figure 9.8). The first part is extremely simple. You can handle the OnResize event of the form and simply copy a couple of values to the two labels. The labels are part of another form, so you need to prefix them with the name of the form instance, Status:
procedure TForm1.FormResize(Sender: TObject);
Status.Label3.Caption := IntToStr(ClientWidth); Status.Label4.Caption := IntToStr(HorzScrollBar.Position);
The output of the Scroll! example
If we wanted to change the output each time the user scrolls the contents of the form, we could not use a Delphi event handler, because there isn't an OnScroll event for forms (although there is one for stand-alone ScrollBar components). Omitting this event makes sense, because Delphi forms handle scroll bars automatically in a powerful way. In Windows, by contrast, scroll bars are extremely low-level elements, requiring a lot of coding. Handling the scroll event makes sense only in special cases, such as when you want to keep track precisely of the scrolling operations made by a user.
Here is the code we need to write. First, add a method declaration to the class and associate it with the Windows horizontal scroll message (wm_HScroll): public procedure FormScroll (var ScrollData: TWMScroll); message wm_HScroll;
Then write the code of this procedure, which is almost the same as the code of the FormResize method we've seen before:
procedure TForm1.FormScroll (var ScrollData: TWMScroll); begin inherited;
Status.Label3.Caption := IntToStr(ClientWidth); Status.Label4.Caption := IntToStr(HorzScrollBar.Position); end;
It's important to add the call to inherited, which activates the method related to the same message in the base class form. The inherited keyword in Windows message handlers calls the method of the base class we are overriding, which is the one associated with the corresponding Windows message (even if the procedure name is different). Without this call, the form won't have its default scrolling behavior; that is, it won't scroll at all.
Because in CLX you cannot handle the low-level scroll messages, there seems to be no easy way to create a program similar to Scroll1. This isn't terribly important in real-world applications, as the scrolling system is automatic, and can probably be accomplished by hooking in the CLX library at a lower level.
The scroll bar's Range property can seem strange until you start to use it consistently. When you think about it a little, you'll start to understand the advantages of the "virtual range" approach. First of all, the scroll bar is automatically removed from the form when the client area of the form is big enough to accommodate the virtual size; and when you reduce the size of the form, the scroll bar is added again.
This feature becomes particularly interesting when the AutoScroll property of the form is set to True. In this case, the extreme positions of the right-most and lower controls are automatically copied into the Range properties of the form's two scroll bars. Automatic scrolling works well in Delphi. In the last example, the virtual size of the form would be set to the right border of the last list box. This was defined with the following attributes:
object ListBox6: TListBox Left = 832 Width = 145 end
Therefore, the horizontal virtual size of the form would be 977 (the sum of the two preceding values). This number is automatically copied into the Range field of the HorzScrollBar property of the form, unless you change it manually to have a bigger form (as I've done for the Scroll1 example, setting it to 1000 to leave some space between the last list box and the border of the form). You can see this value in the Object Inspector, or make the following test: run the program, size the form as you like, and move the scroll thumb to the right-most position. When you add the size of the form and the position of the thumb, you'll always get 1000, the virtual coordinate of the right-most pixel of the form, whatever the size.
We have just seen that forms can automatically scroll their components. But what happens if you paint directly on the surface of the form? Some problems arise, but their solution is at hand. Suppose that we want to draw some lines on the virtual surface of a form, as shown in Figure 9.9.
Since you probably do not own a monitor capable of displaying 2000 pixels on each axis, you can create a smaller form, add two scroll bars, and set their Range property, as I've done in the Scroll2 example. Here is the textual description of the form:
object Form1: TForm1
HorzScrollBar.Range = 2000 VertScrollBar.Range = 2000 ClientHeight = 336 ClientWidth = 472 OnPaint = FormPaint end
The lines to draw on the virtual surface of the form
If we simply draw the lines using the virtual coordinates of the form, the image won't display properly. In fact, in the OnPaint response method, we need to compute the virtual coordinates ourselves. Fortunately, this is easy, since we know that the virtual X1 and Y1 coordinates of the upper-left corner of the client area correspond to the current positions of the two scroll bars:
procedure TForm1.FormPaint(Sender: TObject); var
X1, Y1: Integer; begin
X1 := HorzScrollBar.Position; Y1 := VertScrollBar.Position;
// draw a yellow line Canvas.Pen.Width := 30; Canvas.Pen.Color := clYellow; Canvas.MoveTo (30-X1, 30-Y1); Canvas.LineTo (1970-X1, 1970-Y1); // and so on ...
As a better alternative, instead of computing the proper coordinate for each output operation, we can call the SetWindowOrgEx API to move the origin of the coordinates of the Canvas itself. This way, our drawing code will directly refer to virtual coordinates but will be displayed properly:
procedure TForm2.FormPaint(Sender: TObject); begin
SetWindowOrgEx (Canvas.Handle, HorzScrollbar.Position, VertScrollbar.Position, nil);
This is the version of the program you'll find in the source code on the CD. Try using the program and commenting out the SetWindowOrgEx call to see what happens if you don't use virtual coordinates: You'll find that the output of the program is not correct—it won't scroll, and the same image will always remain in the same position, regardless of scrolling operations. Notice also that the Qt/CLX version of the program, called QScroll2, doesn't use virtual coordinates but simply subtracts the scroll positions from each of the hard-coded coordinates.
Was this article helpful?