How do I know when I've stopped scrolling a TScrollBar? - delphi

I've used some programs with scroll bars that update the linked content while you're still dragging the "thumb", and others that don't until you release the mouse. This implies that there are different types of Windows messages involved here. But all I can find from TScrollBar is an OnScroll event which fires continually while you're dragging. It also doesn't have a OnMouseDown or OnMouseUp event. Is there any way to set up an "OnEndDragging" notification for a TScrollBar?

Try this code (tested with Delphi 2009), it will fill the form client area with a random colour while you track the thumb, and fill it in yellow when the thumb is released:
procedure TForm1.ScrollBar1Scroll(Sender: TObject; ScrollCode: TScrollCode;
var ScrollPos: Integer);
begin
Randomize;
if ScrollCode = scTrack then
Color := RGB(Random(256), Random(256), Random(256));
if ScrollCode = scEndScroll then
Color := clYellow;
end;
The TScrollCode values map to the WPARAM values that you will find documented for WM_HSCROLL and WM_VSCROLL.

Programs that update their scrolling region "live" as the user drags the thumb are handling the sb_ThumbTrack code for the wm_HScroll and wm_VScroll messages. Those that only update when the user releases the thumb are handling the sb_ThumbPosition code.
There's a compromise on those two options, which is to update after the user hasn't moved the thumb for a little bit, even if the user hasn't actually released it yet. Handle sb_ThumbTrack, and then set a timer. If the timer fires, update the display. If another sb_ThumbTrack arrives, reset the timer.

Related

What is the equivalent OnScroll Event to TScrollBar for FMX?

I have not come across a solution for an Event (OnScroll) handler for a TScrollBar under Delphi 10 Seattle. I chose it because it is not a TScrollBox for which I will put any content into (like a Panel or DBGrid). It is merely going to detect the mouse action (None of the MouseUp/Down/Side-to-Side have worked either) and it is going to be used as file seek tool for which it will force a bunch of TeeCharts to update, based on the direction of the scroll.
So far I have only come across incomprehensible examples consisting of WindowsMessages (WM_HScroll) and VLC.StdCtrls.TScrollBar.OnScroll
What is the efficient way to detect the ScrollBar moving besides OnChange?
It would help to be able to manipulate the ScrollBar and have it reset itself in the middle. Much like it would if I set the value much like if I set the Min = -50 and Max = 50 (0)
To overcome the limitations of the FMX TScrollBar, you could use the following as a basis for further enhancement (partly inspired by the link you provided).
If the control(s) you will scroll with the scrollbar has (or can be equipped with) a property to store current scroll position you may use/define such, of type single. For testing I just defined it as a private property of the form, and called it sbIncremental.
I set the properties of the TScrollBar as Orientation = Vertical, SmallChange = 10, Max = 100 and Min = -100
Then, the OnChange event looks like this in my test:
procedure TForm6.ScrollBarChange(Sender: TObject);
var
sb: TScrollBar;
sbOnChange: TNotifyEvent;
begin
sb := (Sender as TScrollBar);
sbIncremental := sbIncremental + sb.Value;
Label1.Text := FloatToStr(sbIncremental); // Use the new value
sbOnChange := sb.OnChange; // Disable OnChange event
sb.OnChange := nil; // -"-
sb.Value := (sb.Min + sb.Max) / 2; // Reset position
sb.OnChange := sbOnChange; // Re-enable OnChange event
end;
To reset the ScrollBar position to the center, we need to disable the OnChange event temporarily.
Update after comments.
As I now understand, your issue is that the visual appearance of the scrollbar doesn't return to the zero position even if it's value is changed (within OnChange) to zero (with min=-100 and max=100 the zero position is in the middle). It does when clicking on the arrow buttons, but it does not if dragging the thumbgrip or clicking on either side of the thumbgrip. Visual update seems to be prevented in these two cases within the OnChange event. Also, calling ScrollBar.Repaint does not update the visual appearance. I found no way to use the OnMouseDown/OnMouseUp events. They don't appear to be linked internally?
This leaves us with following (hacky) solution: Fire a timer with a small delay, say 300 ms. When timeout occurs, the scrollbar is ready to accept new value changes and update visually. The timer also has the positive effect, that the thumbgrip moves as you click, and then moves back. Without any time between there would be no visual indication that something happens.
The OnChange event handler when using a timer
procedure TForm6.ScrollBarChange(Sender: TObject);
begin
// Use the scrollbar value
Label1.Text := FloatToStr((Sender as TScrollBar).Value);
// Enable reset timer
ScrollBarTimer.Enabled := False;
ScrollBarTimer.Enabled := True;
end;
And the OnTimer event
procedure TForm6.ScrollBarTimerTimer(Sender: TObject);
var
sbOnChange: TNotifyEvent;
begin
ScrollBarTimer.Enabled := False;
sbOnChange := ScrollBar.OnChange; // store OnChange event
ScrollBar.OnChange := nil; // disable OnChange event
ScrollBar.Value := (ScrollBar.Min + ScrollBar.Max) / 2; // reset position
ScrollBar.OnChange := sbOnChange; // re-enable OnChange event
end;

Count refreshes to TListView

I've got a TListView to which I may add anything from none to several hundred items depending on the day the user has selected from the log file. I use this code to prevent unnecessary refreshes:
listEvents.Items.BeginUpdate();
listEvents.Items.Clear();
// Add events
listEvents.Items.EndUpdate();
Even so, on my fast development PC I can see a few fast flickers of the list. On the (much slower) production PCs, the flicker is noticeable and rather ugly. My question is there any way to count the number of refreshes to the TListView by hooking into an event? I could then increment a variable and display the value of the variable on a label while I debug this. I tried the TListView::OnDrawItem event but that wasn't called at all.
I suspect you are not using the virtual listview. Use the virtual listview approach to display data. Set OwnerData property to true and handle your display in OnData event. That should prevent the flicker. Pseudo code for this would be:
procedure TForm1.ListView1Data(Sender: TObject; Item: TListItem);
begin
Item.Caption := FloatToStr(Item.Index + 1);
Item.SubItems.Add('Your data here');
end;
Try something like this to avoid flicker:
// Disable
SendMessage(listEvents.Handle, WM_SETREDRAW, Integer(False), 0);
try
listEvents.Items.BeginUpdate();
listEvents.Items.Clear();
// Add events
listEvents.Items.EndUpdate();
finally
// enable
SendMessage(listEvents.Handle, WM_SETREDRAW, Integer(True), 0);
end;
You may no longer be necessary to use BeginUpdate and EndUpdate.
Regards.

How to make a Drawn Line on a Form Invisible in Delphi 7

I am trying to make an analog clock, wherein I would like to make my 'seconds' Line Invisible when the Seconds Changes. I have tried to set the Pen mode to pmNotCopy but it only gives Inverse of Pen Color. What Property must be set in this Form1.Canvas.Pen.Mode:=<Blank> so that My Line Disappears?
Any other Ideas are also appreciated.
Thanks
Modern computers are very fast. If I were you, I'd definitely draw the entire clock from scratch every second. Problem solved. In addition, if you need anti-aliasing, then a simple pen mode trick will never work.
(If you are not using a remote desktop, you might want to employ double-buffering.)
I don't know anything about delphi but just some out if the box thinking:
couldn't you change the color of your line to the background color, making it 'invisible'
You were close. You need to use pmXOR.
Try this:
Create a new Delphi VCL Forms application. Drop a TButton on the bottom of the form (Button1).
Add the code below to the Button1Click event.
Run the application. Click the button, and three parallel lines will be drawn across the top. Click the button again, and the three lines will disappear.
procedure TForm1.Button1Click(Sender: TObject);
var
i: Integer;
begin
Canvas.Pen.Mode := pmXor;
Canvas.Pen.Color := clGreen;
for i := 1 to 3 do
begin
Canvas.MoveTo(50, i * 20);
Canvas.LineTo(Width - 50, i * 20);
end;
end;
All drawing should normally be done in the OnPaint event; I'm intentionally drawing in the Button1Click event for demonstration reasons only.
Just as a note: You should never use Form1 inside an event handler for that form. Referencing the Form1 variable prevents you from creating more than one instance of the form. Use Self instead, and that will automatically refer to the instance the code is running in.

MouseWheelDown event fires multiple times in succession

Delphi 7 on Windows 7
I want to scroll down a list (TElTree) using the mouse wheel... so I start writing some code in the ElTree's OnMouseWheelDown event. Then I notice that it's scrolling 2 rows at once. The mouse wheel settings in Control Panel are set to scroll only 1 line at a time. If I place a breakpoint inside the event handler, I discover that the event handler is itself being executed twice in quick succession.... Why? How to ensure it only executes once (code please)?
When the wheel scrolls, the OS sends WM_MOUSEWHEEL messages. The high-order word of the wParam parameter indicates how far the wheel has turned. If it has turned one "click," then its value will be 120, or WHEEL_DELTA. But it might be less than that if your scroll wheel recognizes scrolling less than a click's worth.
Correct WM_MOUSEWHEEL message handlers need to consider that parameter and either keep a "scrolling accumulator" to keep track of how far the wheel has scrolled or else have the ability to scroll less than a full line. Message handlers that assume that each message signifies a full click of the wheel will be sorry.
Solution is quite simple: After calling the function you want to perform OnMouseWheel you must set Handled:=True so that the routine is not called multiple times. e.g.
procedure TMainForm.FormMouseWheelUp(Sender: TObject; Shift: TShiftState;
MousePos: TPoint; var Handled: Boolean);
begin
YourFunctionToExecute(Sender, Shift, MousePos, Handled);
Handled:=True;
end;

How to stop a UI from locking up when a second form is shown?

I have a Chat program written in Delphi7. As a special "effect" we display a few bullet holes when a particular gunshot sound file is played. We did this by drawing a new form in the shape of the bmp image file of the bullet hole, with a timed delay of a few secs for it to be visible and then fade away.
All of this works, however, while the bullet hole images are onscreen, the program is effectively locked up... returning focus back to the user when the last of the images has faded away.
My programmer isn't real well versed in graphics and believes this is just the price you have to pay to get this effect, but I'm hoping that's not quite true... any suggestions of a better way to randomly display bullet hole images across the screen?
It's hard to say without seeing code and knowing exactly how your opening/loading the second form, but it sounds like you are opening them with ShowModal which will lock the parent form until the modal form returns a result.
If that is the case then you can simple open it with the Show method and then set the focus back to the main form like so.
procedure TForm1.Button1Click(Sender: TObject);
var obj:TForm2;
begin
obj := TForm2.Create(nil);
try
obj.FormStyle := fsStayOnTop;
obj.show;
Self.SetFocus; //set focus back to the form1
except
FreeAndNil(obj);
end;
end;
The above also amuses that you are creating the form dynamically at runtime and that the second form is responsible for freeing itself.
Overlaying grahics on a Form isn't exactly trivial but I think this is what the TCustomTransparentPanel (name from memory) is for.
But anyway, it shouldn't slow down or freeze the program.
Opening a second form really is not the way to do this. Perhaps a picture control with a partially transparent image sized to just be bigger than your bullet hole that you could place anywhere on the form. If you can't do a transparent control, you could take a snapshot of that section of the form and place the bullet hole on top.
When the bullet should appear, you make the picture visible and bring to front
Start a timer for two seconds and tell the picture control to refresh
When the timer goes off, hide the bullet picture
If you want to animate the fade out, just replace the picture with different ones at different times using the timer going off at different times
At 0 seconds make picture visible
At 1.5 seconds replace image with a faded version of the picture
At 2 seconds hide the picture
Using this, there is no need for another form, and the app remains responsive.
I believe what is needed would be to use the SetWinRegion method to draw an outline around the "bullet hole". That way they can be clicked thru, but the holes themselves would be floating on top (provided the previous answer of fsStayOnTop is used).
Even a flash image would require a form of some type to act as the container for the image. It all comes down to creating a window and blasting the image to that window. It doesn't have to "lock" the screen, if you set it to always on top and just "SHOW" it (not ShowModal) it should appear and float. I would then use a timer to control when it is destroyed. The advantage of the SetWinRegion, is you can create a window that has an irregular shape, and the only place the user would not be able to click would be the portion of your window which contained the actual form (inside the region).
If the animations of bullet holes are written as a loop that executes until done then that might be the issue. If so, then try inserting Application.ProcessMessages somewhere in the loop to allow other things to happen while the animation is occurring.
Here's a solution that works. You'd want to wrap the firing process into a timer or some other event driven process.
Form Definition:
TForm1 = class(TForm)
btnTurnOn: TButton;
ListBox1: TListBox;
btnTurnOff: TButton;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure btnTurnOnClick(Sender: TObject);
procedure btnTurnOffClick(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
F:TForm;
Bullet:TBitmap;
x,y:integer;
procedure SubFormPaint(Sender: TObject);
end;
Code:
procedure TForm1.btnTurnOnClick(Sender: TObject);
begin
if F=nil then
begin
F:=TForm.Create(self);
F.Parent:=self;
F.FormStyle:=fsStayOnTop;
F.BorderStyle:=bsNone;
F.Left:=x;
F.Top:=y;
F.Width:=Bullet.Width;
F.Height:=Bullet.Height;
F.OnPaint:=Self.SubFormPaint;
F.Show;
end
else
begin
inc(x,5);
inc(y,5);
F.Left:=x;
F.Top:=y;
F.Invalidate;
end;
end;
procedure TForm1.btnTurnOffClick(Sender: TObject);
begin
F.Free;
F:=nil;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
F:=nil;
Bullet:=TBitmap.Create;
Bullet.LoadFromFile('C:\source\glyfx\glyfx\Emoticon\BMP\16x16\wink_16.bmp');
x:=1;
y:=1;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
Bullet.Free;
end;
procedure TForm1.SubFormPaint(Sender: TObject);
begin
if F<>nil then
F.Canvas.Draw(1,1,Bullet);
end;
See http://msdn.microsoft.com/en-us/library/dd183353(VS.85).aspx, it has a good example of the API calls for alpha blending bitmaps together. For each frame of an animation, you'd probably need to grab a copy of the image that represents your main screen, and then blend in your glyphs with this at the appropriate transparency level.
As previously posted, you could do Application.ProcessMessages repeatedly, although I prefer doing it backwards and using PostMessage to trigger the next frame of the animation.
You'd use ProcessMessages in a loop, but that call could cause other things to occur while you're in that loop - window moving, minimizing, closing, or triggering your animation event again - before the first animation is finished. This is all fine as long as you're mindful that you're not always necessarily operating within the scope of just your loop.
For that reason I prefer having each frame of animation do a PostMessage to trigger the next frame. It just helps me keep things in perspective.

Resources