Tek-Tips is the largest IT community on the Internet today!

Members share and learn making Tek-Tips Forums the best source of peer-reviewed technical information on the Internet!

  • Congratulations Chris Miller on being selected by the Tek-Tips community for having the most helpful posts in the forums last week. Way to Go!

Smooth continuous scrolling

Status
Not open for further replies.

djjd47130

Programmer
Nov 1, 2010
480
US
I've been contemplating over some more advanced features in user experience for the GUI. I have a special list I'm making - a message log rather which replicates the SMS text messages on an iPhone. I have another post in this forum related to this project, but different subject. In the iPhone's control, if you glide your finger up/down the list and quickly let go, the list continues scrolling in the direction you moved it and gradually gets slower and slower until it stops, or until you tap your finger on it again, which stops it.

I have put the ability to use the mouse to drag/scroll in this list. I'm utilizing OnMouseDown, OnMouseUp, and OnMouseMove to make this possible. Everything is working inside a TScrollBox control. When scrolling, I change TScrollBox.VertScrollBar.Position depending on how far the Y position has moved since the mouse button was first pressed. Only part missing is how to keep the box scrolling once the user releases the mouse button. The speed of movement should also depend on how quickly the user moved the mouse before letting go of the button. After releasing the mouse button, the list should continue scrolling (in this case the TScrollBox.VertScrollBar) and gradually slow down until it comes to a halt, or until the user clicks somewhere in the control again. If the user released the mouse button after the mouse had already stopped moving, then it should not continue moving (still mouse upon release means continuing speed of 0).

I know later versions of Delphi have touch screen capabilities, but I'm in D7, which doesn't. I'm building this on a form, not inside a component. However, I will eventually like to wrap all this functionality into a component once I have it perfected. Here's the relevant code to what I already have working (as far as drag-to-scroll)...

Code:
//Lst: TDrawGrid = placed within TScrollBox (no scroll bar - control is larger than TScrollBox)
//Box: TScrollBox = (showing vertical scroll bar to be scrolled)
//fMouseStart: Integer = Starting Y position of mouse when mouse button is clicked
//fScrollStart: Integer = Starting Y position of scroll bar when mouse button is clicked
//fMouseDown: Bool = Master flag to determine if TScrollBox shall scroll upon mouse movement

procedure TfrmConversation.LstMouseDown(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
  fMouseStart:= Y; //Record starting Y position
  fScrollStart:= Box.VertScrollBar.Position; //Record starting scroll bar position
  fMouseDown:= True; //Record event of mouse button being held down
  Screen.Cursor:= crSizeAll; //Change cursor to sizing icon to show user is in drag-to-scroll mode
end;

procedure TfrmConversation.LstMouseUp(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
  fMouseDown:= False; //Record event of mouse button being released (no longer held down)
  Screen.Cursor:= crDefault; //Change cursor back to default icon to show user is no longer in drag-to-scroll mode
end;

procedure TfrmConversation.LstMouseMove(Sender: TObject;
  Shift: TShiftState; X, Y: Integer);
begin
  if fMouseDown then begin //Only continue if user is currently in drag-to-scroll mode
    Box.VertScrollBar.Position:= Box.VertScrollBar.Position + (fMouseStart - Y); //Move scroll bar by mouse distance
  end;
end;


JD Solutions
 
The main thing that seems missing is that you need to keep track of time (GetTickCount), because this will then give you a speed value of how fast the scroll occurred, and therefore at what speed and deceleration it should continue to scroll after the mouse has been released.

If you want the control to remain responsive during the deceleration scroll (and you will), you'll need to handle the deceleration part in a separate thread, and monitor the scrollbox for any further clicks so that it can be interrupted.
 
Thanks for the advice - I don't really think I would need a thread to make it responsive. Just a timer. I hate trying to make threads. Obviously a direct loop in the code will be a huge mistake. But using a timer to track the time should do the trick. The question is, how to make this formula to calculate the speed based on the user's movements?


JD Solutions
 
The issue you'll have is that if you perform the deceleration in a TTimer method, the application will be unresponsive to the user clicking and scrolling the other way to stop it if they want to. It would be very frustrating to use. You'd scroll, and you'd see what you want and release and the control would continue scrolling off the screen, and not respond to you at all. Plus, creating a thread to handle it means it will be easier to package all of this up into it's own control unit and have no effect on anything else on the screen.

How would you go about implementing if you had 5 or 10 of these controls visible and usable at once on a form? If you develop for that kind of use, I think you'll end up with a much better stable control.

Use the GetTickCount and record (tca) in OnMouseDown and also MouseStart.Y (mya), and again in OnMouseUp (tcb, myb). Your formula for speed = (mya - myb) / (tcb - tca). If speed is negative, the user scrolled upwards. Speed equals the number of Y positions scrolled per Tick.

The actual decelation part I don't have an immediate answer. Google for deceleration formula to give yourself an idea of what you need to figure out. Your thread is going to loop continuously until the speed is 0 (decelartion is finished), so you will continuously reduce it while adjusting the Y position.

If I were doing it, I'd plunge ahead with the structure and then muck around with the formula until it felt right. What looks good on paper may not translate very well to the interface.
 
I'm not measuring speed in how far the mouse moved from mouse-down to mouse-up. The speed should depend on how many pixels per second was the user moving the mouse when the mouse button was released. If this is 0, that means the user wasn't moving the mouse, and therefore should not scroll. This is where the timer comes in - the timer will gradually decrease this speed. Again not a loop, a timer. The timer its self will set the scrolling position, as well as gradually decrease the speed. It may even work in two timers - because the movement will have to be quicker and swifter than the rate of decreasing the speed. As for the user clicking to stop, this is also not a big deal, again, because it's using a timer, not a loop. I would have another global variable 'fAutoscroll' for example. When user releases mouse, fAutoscroll is set to True, and the timer takes over the movement. By the time the speed becomes 0, fAutoscroll will be set to false. If the user clicks inside there in the mean-time, then it will also set fAutoscroll to False. In the timer, I will always check fAutoscroll and if it is False, I will halt everything.

Also please note that every attempt I have ever made at a thread has failed miserably. I've had plenty of help, but I always wind up doing something wrong and screwing it up to a point where I can't debug the problem.

JD Solutions
 
I don't want to sidetrack onto threads, but they are an essential tool you need to have in your bag as a programmer. Try something easy such as a button that creates and launches a TThread that simply updates a label on a form with an incrementing number. Have another button stop the thread, and/or allow the thread to stop at a certain number.

There's not much involved in getting a test project like that working.

The next step is to make the thread customisable, so that your button press creates 3 of that TThread object at once, and each updates a different label on the form. Have a separate button for each thread stop it.

Again, there's not much involved, but it will illustrate how easy they are to get working.
 
I can make a thread, no problem, and it runs. My problem is figuring out the best ways to connect objects between them, or 'synchronize' things. Also, knowing the best place to store variables etc. which need to be shared between the form and thread, and maybe even between multiple threads at once. It's the concept of making the thread work with the outside world which I suck at.

Anywho, I'll plug in the formula you mentioned and see where I can get.

JD Solutions
 
For sharing variables between the main thread and your own threads, add public properties to your TThread object - that is where you store things that the main thread can access.

For variables that all threads can access, add a global variable in the implementation section - however you will need to use locking.

for example

Code:
[b]implementation[/b]

[b]var[/b]
  Counter: Integer;

[b]procedure[/b] TMyThread.Execute;
[b]begin[/b]
  [navy][i]// snip
[/i][/navy]  [b]if[/b] Counter < [purple]10[/purple] [b]then[/b]
    Inc(Counter);
  [navy][i]// snip
[/i][/navy][b]end[/b];

is no good. Other instances of this thread object may be running and may run at different speeds/priorites. You cannot assume that because your thread is satisfied that Counter < 10 that when it goes to increment it in the next statement that it will still be < 10. You need to use locking.

Something like this

Code:
[b]implementation[/b]

[b]uses[/b]
  SyncObjs;

[b]var[/b]
  Lock: TCriticalSection;
  Counter: Integer;

[b]procedure[/b] TMyThread.Execute;
[b]begin[/b]
  [navy][i]// snip
[/i][/navy]  Lock.Acquire;
  [b]try[/b]
    [b]if[/b] Counter < [purple]10[/purple] [b]then[/b]
      Inc(Counter);
  [b]finally[/b]
    Lock.Release;
  [b]end[/b];
  [navy][i]// snip
[/i][/navy][b]end[/b];

[b]initialization[/b]
  Lock := TCriticalSection.Create;

[b]finalization[/b]
  Lock.Free;

[b]end[/b].

Only one thread can enter the locked area at a time, all other threads will suspend at the Lock.Acquire point until the Lock object is released.

If you'd like to talk about threads any further, open up a new forum thread to discuss.
 
Wow, there's always a first for everything - I have never ever made any use of initialization or finalization - other than the automatic use of it for things like ActiveX. I've heard of TCriticalSection, but never knew how it worked. That makes sense. Thank you much!

JD Solutions
 
I tried this formula above for continuing the scroll, and I hit a problem. Technically speaking, 'Y' in both the OnMouseDown and OnMouseUp events are identically the same number, because it's still on the same canvas in the same place. The difference is the scrolled position of the TScrollBox - not necessarily the Y value. I'll dig a little further, try to change which number it records to use TScrollBox.VertScrollBar.Position instead of Y in the event handler.

JD Solutions
 
Status
Not open for further replies.

Part and Inventory Search

Sponsor

Back
Top