Menu rollovers flicker - How to account for Mouse Leave activity - delphi

I'm building a custom menu and have been having some issues with the hover states on the links. Now, after much tinkering I've managed to get my menu rectangle responding properly to mouse hover states - Almost.
I can't for the life of me work out how to get them to revert back to normal once the mouse has left the rectangle - it remains in it's hovered state. Moving onto a different rectangle resets correctly, anywhere else on the canvas is treated as still being in last rectangle hovered over.
My MouseMove procedure.
procedure TOC_MenuPanel.MouseMove(Shift:TShiftState; X,Y:Integer);
var
i : integer;
begin
pt := Mouse.CursorPos;
pt := ScreenToClient(pt);
for i := 0 to High(MenuRects) do
begin
if PtInRect(MenuRects[i], Point(X, Y)) then
begin
chosenRect := i;
Refresh;
end
else
begin
chosenRect := -1;
end;
end;
inherited;
end;
And my paint procedure:
procedure TOC_MenuPanel.Paint;
var
// TextStyle: TTextStyle;
R, itemR: TRect;
count : Integer;
x1,y1,x2,y2 : Integer;
begin
// Set length of array
SetLength(MenuRects, fLinesText.Count);
// Set TRect to Canvas size
R := Rect(5, 5, Width-5, Height-5);
x1 := 10;
y1 := 10;
x2 := Width-10;
inherited Paint;
with Canvas do begin
// Set fonts
Font.Height := MenuFontHeight;
Font.Color := clWhite;
// Draw outerbox
GradientFill(R, clLtGray, clWhite, gdVertical);
// Draw inner boxes
if fLinesText.Count = 0 then exit
else
for count := 0 to fLinesText.Count - 1 do
begin
// Define y2
y2 := TextHeight(fLinesText.strings[count])*2;
itemR := Rect(x1, y1, x2, y2*(count+1));
Pen.color := clGray;
// Test against chosenRect value and compare mouse position against that of the rectangle
if (chosenRect = count) and (PtInRect(MenuRects[count], pt)) then
Brush.color := stateColor[bttn_on]
else
Brush.color := stateColor[bttn_off];
Rectangle(itemR);
// Push rectangle info to array
MenuRects[count] := itemR;
// Draw the text
TextRect(itemR, x1+5, y1+5, fLinesText.strings[count]);
// inc y1 for positioning the next box
y1 := y1+y2;
end;
end;
end;

The painting that you do in the mouse move event handler is immediately lost because you force a paint cycle by calling Invalidate. As a general rule, it is best to do all painting to the screen in a paint cycle. In some scenarios it can make sense to paint outside of a paint cycle, but it is notoriously hard to get right.
So, I suspect that you need to move all the drawing code into your paint routine, wherever and whatever that is. So, in the mouse move event you need to invalidate the form or paint box or whatever it is that paints the scene. Then in your paint routine you use GetCursorPos, or Mouse.Pos or similar to find the position of the cursor. And you use that to determine how to paint the scene. You may well find it more effective, in terms of flicker avoidance, to paint to an off-screen bitmap and then blit that onto the canvas.
Now, if you invalidate on every single mouse move then you might find the painting burden to be excessive. So perhaps you should keep track of the state of the most recently painted scene. Test in the mouse move handler whether or not the new state differs from the most recently painted. Only if it does differ would you force a paint cycle.

The error was in the MouseMove procedure, the following produces the correct behaviour:
procedure TOC_MenuPanel.MouseMove(Shift:TShiftState; X,Y:Integer);
var
i : integer;
begin
// Get cursor position within the control
pt := Mouse.CursorPos;
pt := ScreenToClient(pt);
// loop through Array of Rectangles
for i := 0 to High(MenuRects) do
begin
if PtInRect(MenuRects[i], Point(X, Y)) then
begin
chosenRect := i;
Break; // If statement evaluates to true, stop the loop
end
else
begin
chosenRect := -1;
end;
end;
Refresh; // Refresh the canvs
inherited;
end;

Related

How detect if i'm moving mouse to left, right, top or bottom inside TImage component on mousemove event?

I want know how detect to what side i'm moving mouse: to left, right, top, bottom inside TImage component on mousemove event?
Thank you.
Here's an example to be used in an FMX project. For a VCL project, you would use integer variables.
First, declare two variables Xold, Yold: single; for example in the private section of the form.
private
Xold, Yold: Single;
Initialize these variables e.g. in the forms OnCreate() event. Using NaN requires System.Math in the uses clause.
procedure TForm5.FormCreate(Sender: TObject);
begin
Xold := NaN;
Yold := NaN;
end;
Then, in the OnMouseMove() event, calculate the movement horizontally and vertically, negative value indicate moving left or up, positive right or down.
procedure TForm5.Image1MouseMove(Sender: TObject; Shift: TShiftState; X,
Y: Single);
var
horz, vert: Single;
begin
if not IsNan(Xold) then horz := X - Xold else horz := 0;
if not IsNan(Yold) then vert := Y - Yold else vert := 0;
Xold := X; // save new values
Yold := Y; //
// use horz and vert as needed
Label1.Text := Format('h: %f, v: %f',[horz, vert]);
end;
You may also want to reset the Xold and Yold variables to NaN when the mouse leaves the image.
procedure TForm5.Image1MouseLeave(Sender: TObject);
begin
Xold := NaN;
Yold := NaN;
end;
It was asked in comments, why initialize to NaN instead of just zero? Xold := 0; Yold := 0 is the top-left corner. If the mouse entry to the image happens at e.g. right side, the first move would be a jump from 0 to image width. Using NaN we can omit the first entry as a move and just store the entry point in Xold and Yold for use with next move.

Canvas.textout doesn´t show text after a new series is made visible

So what i'm doing is display the x and y values of the mouse pointer on a teechart chart using the following code, inside the onmousemove event:
oscilografia.Repaint;
if ((x>236) and (x<927)) and ((y>42) and (y<424)) then
begin
oscilografia.Canvas.Brush.Style := bsSolid;
oscilografia.Canvas.Pen.Color := clBlack;
oscilografia.Canvas.Brush.Color := clWhite;
oscilografia.Canvas.TextOut(x+10,y,datetimetostr(oscilografia.Series[0].XScreenToValue(x))+','+FormatFloat('#0.00',oscilografia.series[0].YScreenToValue(y)));
edit1.Text:=inttostr(x)+' '+inttostr(y);
end;
The code works fine, but a problem happens when i make another series visible by selecting it on the legend: the text inside the box created by canvas.textout isn´t shown anymore.
The box is still there following the mouse, but without any text. So i would like a solution to this.
The basic problem is down to how painting works. Windows do not have persistent drawing surfaces. What you paint onto a window will be overwritten the next time the system needs to repaint it.
You need to arrange that all painting is in response to WM_PAINT messages. In Delphi terms that typically means that you would put your painting code in an overridden Paint method.
So the basic process goes like this:
Derive a sub-class of the chart control and in that class override Paint. Call the inherited Paint method and then execute your code to display the desired text.
In your OnMouseMove event handler, if you detect that the mouse coordinates text needs to be updated, call Invalidate on the chart.
The call to Invalidate will mark that window as being dirty and when the next paint cycle occurs, your code in Paint will be executed.
What is more, when anything else occurs that forces a paint cycle, for instance other modifications to the chart, your paint code will execute again.
Note, as an alternative to sub-classing, you can probably use the TChart event OnAfterDraw. But I'm not an expert on TChart, so am not sure. The main points though are as I state above.
From a comment you wrote, I see you followed this example.
Note it doesn't draw any rectangle; it only draws text, so I'm not sure to understand what box is following your mouse.
Also note the example calls Invalidate, as David Heffernan suggested in his answer.
Find below a modified version of the same example, painting a rectangle before the text.
procedure TForm1.FormCreate(Sender: TObject);
begin
Series1.FillSampleValues(10);
Chart1.View3D := False;
end;
procedure TForm1.Chart1MouseMove(Sender: TObject; Shift: TShiftState; X,
Y: Integer);
var tmpL,tmpL2,ClickedValue : Integer;
tmpWidth, tmpHeight: Integer;
tmpText: string;
begin
clickedvalue := -1;
tmpL2:= -1;
With Chart1 do
begin
If (Series1.Clicked(X, Y) <> -1) And (not OnSeriesPoint) Then
begin
Canvas.Brush.Style := bsSolid;
Canvas.Pen.Color := clBlack;
Canvas.Brush.Color := clWhite;
tmpText:=FormatFloat('#.00',Series1.XScreenToValue(x))+','+FormatFloat('#.00',Series1.YScreenToValue(y));
tmpWidth:=Canvas.TextWidth(tmpText)+10;
tmpHeight:=Canvas.TextHeight(tmpText);
Canvas.Rectangle(x+5, y, x+tmpWidth, y+tmpHeight);
Canvas.TextOut(x+10,y,tmpText);
OnSeriesPoint := True;
ClickedValue:= Series1.Clicked(x,y);
End;
//Repaint Chart to clear Textoutputted Mark
If (ClickedValue=-1) And (OnSeriesPoint) Then
begin
OnSeriesPoint := False;
Invalidate;
End;
tmpL := Chart1.Legend.Clicked(X, Y);
If (tmpL <> -1) And ((tmpL <> tmpL2) Or (not OnLegendPoint)) Then
begin
repaint;
Canvas.Brush.Color := Series1.LegendItemColor(tmpL);
Canvas.Rectangle( X, Y, X + 20, Y + 20);
Canvas.Brush.Color := clWhite;
Canvas.TextOut(x+15,y+7,FormatFloat('#.00',Series1.XValues.Items[Series1.LegendToValueIndex(tmpl)]));
tmpL2 := tmpL;
OnLegendPoint := True;
End;
If (tmpL2 = -1) And (OnLegendPoint) Then
begin
OnLegendPoint := False;
Invalidate;
End;
End;
End;

Graphics32 layers performance issues

I developed an application in Delphi using graphics32 library. It involves adding layers to a ImgView32 control. It does all I want now, except that when the user adds more that 25-30 layers to the ImgView, the selected layer starts behaving badly. I mean,
- when there are 30+ layers on the ImgView32 and I click on a layer, it takes about 2.5-2 seconds to actually select it.
- Also when I try to move the layer, it moves abruptly
It appears that ImgViewChange is called way too many times when there are more layers. Same goes to PaintLayer. It gets called way too many times.
How can I stop that from happening? How can I make the layers move graciously even when there are more that 30 layers added?
My code is as follows:
procedure TMainForm.LayerMouseDown(Sender: TObject; Buttons: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
cronstart:=now;
if Sender <> nil then
begin
Selection := TPositionedLayer(Sender);
end
else
begin
end;
cronstop:=now;
Memo1.Lines.Add('LayerMouseDown:'+FloatToStr((cronstop-cronstart)*secsperDay)+'sec');
end;
procedure TMainForm.AddSpecialLineLayer(tip:string);
var
B: TBitmapLayer;
P: TPoint;
W, H: Single;
begin
B := TBitmapLayer.Create(ImgView.Layers);
with B do
try
Bitmap.SetSize(100,100);
Bitmap.DrawMode := dmBlend;
with ImgView.GetViewportRect do
P := ImgView.ControlToBitmap(GR32.Point((Right + Left) div 2, (Top + Bottom) div 2));
W := Bitmap.Width * 0.5;
H := Bitmap.Height * 0.5;
with ImgView.Bitmap do
Location := GR32.FloatRect(P.X - W, P.Y - H, P.X + W, P.Y + H);
Scaled := True;
OnMouseDown := LayerMouseDown;
B.OnPaint := PaintGeamOrizHandler
except
Free;
raise;
end;
Selection := B;
end;
procedure TMainForm.PaintGeamOrizHandler(Sender: TObject;Buffer: TBitmap32);
var
bmp32:TBitmap32;
R:TRect;
usa2:single;
latime,inaltime,usa:Single;
inaltime2, latime2:single;
begin
cronstart:=now;
if Sender is TBitmapLayer then
with TBitmapLayer(Sender).GetAdjustedLocation do
begin
bmp32:=TBitmap32.Create;
try
R := MakeRect(TBitmapLayer(Sender).GetAdjustedLocation);
bmp32.DrawMode:=dmblend;
bmp32.SetSize(Round(Right-Left), Round(Bottom-Top));
latime:=Round((Right-Left));
inaltime:=Round((Bottom-Top));
usa:=60;
usa2:=usa / 2;
with TLine32.Create do
try
EndStyle := esClosed;
JoinStyle := jsMitered;
inaltime2:=inaltime / 2;
latime2:=latime / 2;
SetPoints([FixedPoint(latime2-usa2,inaltime2), FixedPoint(latime2+usa2,inaltime2)]);
Draw(bmp32, 13, clWhite32);
SetPoints(GetOuterEdge);
Draw(bmp32, 1.5, clBlack32);
SetPoints([FixedPoint(latime2-usa2-3,inaltime2), FixedPoint(latime2-usa2,inaltime2)]);
Draw(bmp32, 5, clBlack32);
SetPoints([FixedPoint(latime2-usa2-3-7,inaltime2), FixedPoint(latime2-usa2-3,inaltime2)]);
Draw(bmp32, 7, clWhite32);
SetPoints(GetOuterEdge);
Draw(bmp32, 1.5, clBlack32);
SetPoints([FixedPoint(latime2+usa2,inaltime2), FixedPoint(latime2+usa2+3,inaltime2)]);
Draw(bmp32, 5, clBlack32);
SetPoints([FixedPoint(latime2+usa2+3+7,inaltime2), FixedPoint(latime2+usa2+3,inaltime2)]);
Draw(bmp32, 7, clWhite32);
SetPoints(GetOuterEdge);
Draw(bmp32, 1.5, clBlack32);
finally
Free;
end;
(Sender as TBitmapLayer).Bitmap.Assign(bmp32);
finally
bmp32.Free;
end;
end;
cronstop:=now;
Memo1.Lines.Add('PaintLayer:'+FloatToStr((cronstop-cronstart)*secsperDay)+'sec');
end;
procedure TMainForm.SetSelection(Value: TPositionedLayer);
begin
if Value<>nil then
begin
if Value <> FSelection then
begin
if RBLayer <> nil then
begin
RBLayer.ChildLayer := nil;
RBLayer.LayerOptions := LOB_NO_UPDATE;
end;
FSelection := Value;
if Value <> nil then
begin
if RBLayer = nil then
begin
RBLayer := TRubberBandLayer.Create(ImgView.Layers);
RBLayer.MinHeight := 1;
RBLayer.MinWidth := 1;
end
else
RBLayer.BringToFront;
RBLayer.ChildLayer := Value;
RBLayer.LayerOptions := LOB_VISIBLE or LOB_MOUSE_EVENTS or LOB_NO_UPDATE;
RBLayer.OnResizing := RBResizing;
end;
end;
end;
end;
procedure TMainForm.RBResizing(Sender: TObject;
const OldLocation: TFloatRect; var NewLocation: TFloatRect;
DragState: TRBDragState; Shift: TShiftState);
var
w, h, cx, cy: Single;
nw, nh: Single;
begin
cronstart:=now;
if DragState = dsMove then Exit; // we are interested only in scale operations
if Shift = [] then Exit; // special processing is not required
if ssCtrl in Shift then
begin
{ make changes symmetrical }
with OldLocation do
begin
cx := (Left + Right) / 2;
cy := (Top + Bottom) / 2;
w := Right - Left;
h := Bottom - Top;
end;
with NewLocation do
begin
nw := w / 2;
nh := h / 2;
case DragState of
dsSizeL: nw := cx - Left;
dsSizeT: nh := cy - Top;
dsSizeR: nw := Right - cx;
dsSizeB: nh := Bottom - cy;
dsSizeTL: begin nw := cx - Left; nh := cy - Top; end;
dsSizeTR: begin nw := Right - cx; nh := cy - Top; end;
dsSizeBL: begin nw := cx - Left; nh := Bottom - cy; end;
dsSizeBR: begin nw := Right - cx; nh := Bottom - cy; end;
end;
if nw < 2 then nw := 2;
if nh < 2 then nh := 2;
Left := cx - nw;
Right := cx + nw;
Top := cy - nh;
Bottom := cy + nh;
end;
end;
cronstop:=now;
Memo1.Lines.Add('RBResizing:'+FloatToStr((cronstop-cronstart)*secsperDay)+'sec');
end;
procedure TMainForm.ImgViewChange(Sender: TObject);
var
wid,hei:Integer;
begin
Edit1.Text:=IntToStr(StrToInt(Edit1.Text)+1);
cronstart:=now;
if Selection = nil then
begin
end
else
begin
wid:=Round(Selection.Location.Right-Selection.Location.Left);
hei:=Round(Selection.Location.Bottom-Selection.Location.Top);
// SelectLayerPan(Selection.Index);
end;
cronstop:=now;
Memo1.Lines.Add('ImgViewChange:'+FloatToStr((cronstop-cronstart)*secsperDay)+'sec');
end;
procedure TMainForm.ImgViewMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer; Layer: TCustomLayer);
begin
Edit1.Text:='0';
cronstart:=now;
if Layer = nil then
begin
if Assigned(FSelection) then
begin
Selection := nil;
RBLayer.Visible:=false;
end;
end
else
begin
// SelectLayerPan(layer.Index);
end;
cronstop:=now;
Memo1.Lines.Add('imgViewMouseDown:'+FloatToStr((cronstop-cronstart)*secsperDay)+'sec');
end;
procedure TMainForm.ImgViewPaintStage(Sender: TObject; Buffer: TBitmap32;
StageNum: Cardinal);
const //0..1
Colors: array [Boolean] of TColor32 = ($FFFFFFFF, $FFB0B0B0);
var
R: TRect;
I, J: Integer;
OddY: Integer;
TilesHorz, TilesVert: Integer;
TileX, TileY: Integer;
TileHeight, TileWidth: Integer;
begin
TileHeight := 13;
TileWidth := 13;
TilesHorz := Buffer.Width div TileWidth;
TilesVert := Buffer.Height div TileHeight;
TileY := 0;
for J := 0 to TilesVert do
begin
TileX := 0;
OddY := J and $1;
for I := 0 to TilesHorz do
begin
R.Left := TileX;
R.Top := TileY;
R.Right := TileX + TileWidth;
R.Bottom := TileY + TileHeight;
Buffer.FillRectS(R, Colors[I and $1 = OddY]);
Inc(TileX, TileWidth);
end;
Inc(TileY, TileHeight);
end;
end;
procedure TMainForm.Button1Click(Sender: TObject);
begin
Edit1.Text:='0';
MainForm.AddSpecialLineLayer('geams'); //orizontal
end;
So just click the button multiple times (30 times) and you will notice the eratic behaviour once you get to have 25-30 layers added.
(Of course use the base code from the layers example of the library and add the above procedures)
Maybe a solution would be to disable somewhere the ImgViewChange event from firing. But I do not know where to do that... Or maybe I'm wrong.
Please give me a solution for this problem... because I can't think of anything...
EDIT
Here is a screenshot that will explain better:
As you can see in the right side of the imgView, there are 3 editboxes. The first tells us that there are 25 layers added already. The other two are also self-explanatory.
In the left side of the picture you can see the layers drawn there. They are all the same, drawn with the paintHandler from the code. So all the layers are identical
Now consider this scenario: no layer is selected, then I start clicking layers, the first 3 clicks, show me ImgViewChange=52 and Paint=26, for each of them. Then on my fourth click on a layer the values are those in the image displayed here. This does not make any sense.
So ImgViewChanged is called 1952 times and the PaintHandler is called 976 times. There must be a bug somewhere...
Please help me figure this out. take into consideration that those editboxes get filled in the code above. Also in this test project there is no other code that might do this crazy behavior. I wrote this test project with only the code that was neccessary to make it work. So the code is above, the behavior is in the picture.
EDIT
After I added bmp32.BeginUpdate and bmp32.EndUpdate in the PaintHandler method, the number of repaints and imgViewChanges seem to have decreased, but not by much. Now I get ImgViewChange=1552 and PaintHandler=776.
I'm not even sure that it's because my change, because these numbers seem almost random. I mean I have no idea why it happens, who triggers those events for regular number of times, and what happens when they are triggered so many more times?
When I add the layers to the imgView, all 25 of them, I leave them where they are added: in the center of the View. After they are all added, I start click-in on each and I drag them away from the center so they would all be visible.
Now, the first 15-20 layers that I click on and drag from the center, the 2 numbers that I monitor (number of times those two events get fired) is a lot lower that the numbers I get after the 20th layer that I want to drag from the center. And after they are all dispersed in the view, it begins: some layers are click-able in real-time, others take a while to get selected and my count of event-fires are through the roof.
EDIT
I found my problem.
With this I reduced the number of events that get fired to the normal amount. So the solution was to add BeginUpdate and EndUpdate for the Assignment of the layer's bitmap...
So in the PaintHandler I changed the code to:
(Sender as TBitmapLayer).BeginUpdate;
(Sender as TBitmapLayer).Bitmap.Assign(bmp32);
(Sender as TBitmapLayer).EndUpdate;
And now my layers behave like they should. Thank you SilverWarrior for pointing me into the right direction. Please convert your comment into an answer so I can accept it.
The BeginUpdate/EndUpdate are beneficial to reduce the number of ImgViewChange events as documented here
OnChange is an abstract change notification event, which is called by
some of the descendants of TCustomPaintBox32 immediately after changes
have been made to their contents. In TCustomImage32, for example, this
includes redirection of change notification events from the contained
bitmap and from layers. This event, however, is not called by
TCustomPaintBox32 control itself, unless you call the Changed method
explicitly. Change notification may be disabled with BeginUpdate call
and re-enabled with EndUpdate call.
However, there are other problems in your code:
In AddSpecialLineLayer() you create a new TBitmapLayer, set the size and location of its Bitmap and set its OnPaint handler to PaintGeamOrizHandler(). This is not a problem in itself, but it's the first step towards the real problem.
In PaintGeamOrizHandler() the main idea seems to be to draw some shapes, but the way it is done is very time consuming for no benefit.
First you create a new TBitmap32. Then you draw the shapes on this bitmap. Then you assign it to the layers bitmap. Finally you free the bitmap just created.
All of the shape drawing could instead have been done directly to the layers bitmap. The "temporary" bitmap is just a waist of CPU resources.
But another question is, why are the shapes drawn every time the layer needs to be painted? The bitmap of the TBitmapLayer is perfectly capable of retaining the shapes until you specifically need to change them. Instead you could have drawn the shapes in a separate procedure as a one time effort when you created the layer (and/or when you need to change the shapes).
You may also want to explore the documentation for paint stages and perhaps repaint optimizer

Avoid TListView drawing/updating while mouse hovering/over

I have a TListView with some modifications. It includes some icons (several, depending on the item) per row, as well as the possibility of a background for a row if certain conditions are met.
It seems to be rendering all right. But a problem occurs when I move the mouse over the window, it seems like the rows are being re-rendered, this creates an unnecessary lag and more importantly, it seems to mess with the visualisation. It should only re-draw if I do something (like select a row).
How do I force it to stop (seemingly refreshing rows upon mouse over)? Currently I am using the AdvancedCustomDrawItem to draw. It also takes like a second for the window to react to a selection of an item, that seems dull.
So basically, each row has DrawText() and drawing images onto the Sender.Canvas. This is admittedly a slow progress, but it works for now, if it just didn't seemingly redraw the rows when I hover over them! In fact, if I use the Aero theme, the rows become black when you hover over them.
Here is my event code on AdvancedCustomDrawItem:
procedure TfrmJobQueue.ListView1AdvancedCustomDrawItem(Sender: TCustomListView;
Item: TListItem; State: TCustomDrawState; Stage: TCustomDrawStage;
var DefaultDraw: Boolean);
const
DT_ALIGN: array[TAlignment] of integer = (DT_LEFT, DT_RIGHT, DT_CENTER);
var
r: TRect;
SL: TStringList;
TypeName: string;
I: Integer;
TypeState: integer;
x1,x2: Integer;
S: string;
begin
if Stage = cdPostPaint then begin
// Ways I tried to avoid it; but failed.
if cdsHot in State then
exit;
if cdsNearHot in State then
exit;
if cdsOtherSideHot in State then
exit;
if cdsMarked in State then
exit;
if cdsIndeterminate in State then
exit;
Sender.Canvas.Brush.Style := bsSolid;
if FRepLines.Items[Item.Index].IsAutoReport then begin
Sender.Canvas.Font.Color := clBlack;
Sender.Canvas.Brush.Color := clSkyBlue;
end else begin
Sender.Canvas.Font.Color := clBlack;
Sender.Canvas.Brush.Color := clWhite;
end;
if cdsSelected in State then begin
Sender.Canvas.Font.Color := clWhite;
Sender.Canvas.Brush.Color := clNavy;
end;
R := Item.DisplayRect(drBounds);
Sender.Canvas.FillRect(R);
Sender.Canvas.Brush.Style := bsClear;
if cdsFocused in State then
DrawFocusRect(Sender.Canvas.Handle, R);
x1 := 0;
x2 := 0;
for i := 0 to TListView(Sender).Columns.Count - 1 do
begin
inc(x2, Sender.Column[i].Width);
r.Left := x1;
r.Right := x2;
if i = 0 then
S := Item.Caption
else
S := Item.SubItems[i-1];
if DT_ALIGN[Sender.Column[i].Alignment] = DT_LEFT then
S := ' ' + S;
DrawText(Sender.Canvas.Handle,
S, length(S), r,
DT_SINGLELINE or DT_ALIGN[Sender.Column[i].Alignment] or
DT_VCENTER or DT_END_ELLIPSIS);
x1 := x2;
end;
r := Item.DisplayRect(drIcon);
SL := TStringList.Create;
SL.CommaText := FRepLines.Value(Item.Index, 'TypeState');
r.Left := Sender.Column[0].Width + Sender.Column[1].Width + Sender.Column[2].Width + Sender.Column[3].Width
+ Sender.Column[4].Width;
for I := 0 to SL.Count - 1 do begin
if GetTypeImagesIndex(SL.Names[I]) = -1 then
continue;
// FRepLines is a collection of items containing more information about each row.
if FRepLines.Value(Item.Index, 'State') <> '1' then begin // no error
TypeName := SL.Names[I];
TypeState := StrToIntDef(SL.Values[TypeName], 0);
// State*Images are TImageList.
if TypeState = 0 then
StateWaitingImages.Draw(Sender.Canvas, r.Left + 17*I, r.Top,
GetTypeImagesIndex(TypeName))
else
StateDoneImages.Draw(Sender.Canvas, r.Left + 17*I, r.Top,
GetTypeImagesIndex(TypeName));
CreateIconToolTip(StrToIntDef(FRepLines.Value(Item.Index, 'RepJob'), -1),
TypeName, r.Left + 17*I, ListView1.ViewOrigin.Y + r.Top,
Format(TranslateString('RepQTypeState'),
[TranslateString(Format('RepQTypeStateN%s', [TypeName])),
TranslateString(Format('RepQTypeState-%d', [TypeState]))]));
end;
end;
end;
end;
Some explanation of the code:
The list is a list of reports (a report queue). I am introducing a concept of 'AutoReports' (or scheduled reports in the UI), which I want to highlight with a light blue background (clSkyBlue).
In addition to that background, it also draws some icons on the Status-column, which indicates what stages the report are in and moreover, what formats a report has been ordered in (formats like PDF, Excel and HTML), and whether it has been printed and/or emailed. An icon only appears if such an event has been ordered, so the number of icons are variable.
The waiting state images are greyed out versions of the done state images. I have also tried to create some code, so when I hover over the specific icons, it has a tooltip message.
Because the code is rather dull in speed, I suspect I am doing something incredibly wrong.
HotTracking is likely enabled. That causes items to redraw as they are moused over, so the item under the mouse can be rendered differently. You are probably ignoring the hottrack state when drawing. That could account for the blackness.
You should profile your code to find the real bottleneck. Drawing code needs to be fast. I do a lot of custom drawing in a ListView and it does not behave slowly like you describe.
Update: Consider re-writing your code to draw individual columns in the OnAdvancedCustomDrawSubItem event instead of doing everything in the OnAdvancedCustomDrawItem event. Also, you don't need to calculate each column's bounds manually, you can use ListView_GetSubItemRect() instead. And lastly, you are leaking your TStringList.

TVirtualStringTree: Scrolling to the bottom two times causes toggling

I have variable height nodes. If scrolled node height is more than VST client area, calling "ScrollIntoView(GetLast, False, False)" function first time does the job perfectly and it jumps to the end of last node which is good.
But calling same function again causes that scrolling to the beginning of last node.
Is this a kind of feature? I don't want this, how to disable?
I have checked ScrollIntoView function to understand the reason. With the first call R.Top is 0, so it branches to else part which yields expected result.
But with the second call it finds that R.Top is negative, and does if part, which causes to scroll to beginning of the last node which is not desired.
Any suggestion?
This is OnTimer event: (500ms)
procedure TMainForm.SyncHexLog;
begin
Try
if (HexLog.RootNodeCount <> FirpList.ComOperationCountLagged) then
begin
HexLog.RootNodeCount := FirpList.ComOperationCountLagged;
// measure for fast scroling
HexLog.ReInitNode(HexLog.GetLastNoInit(), True);
if FAutoScroll then
begin
//HexLog.ScrollToTheBottom();
HexLog.ScrollIntoView(HexLog.GetLast(), False, False);
end;
end;
Finally
End;
end;
function TBaseVirtualTree.ScrollIntoView(Node: PVirtualNode; Center: Boolean; Horizontally: Boolean = False): Boolean;
// Scrolls the tree so that the given node is in the client area and returns True if the tree really has been
// scrolled (e.g. to avoid further updates) else returns False. If extened focus is enabled then the tree will also
// be horizontally scrolled if needed.
// Note: All collapsed parents of the node are expanded.
var
R: TRect;
Run: PVirtualNode;
UseColumns,
HScrollBarVisible: Boolean;
ScrolledVertically,
ScrolledHorizontally: Boolean;
begin
ScrolledVertically := False;
ScrolledHorizontally := False;
if Assigned(Node) and (Node <> FRoot) then
begin
// Make sure all parents of the node are expanded.
Run := Node.Parent;
while Run <> FRoot do
begin
if not (vsExpanded in Run.States) then
ToggleNode(Run);
Run := Run.Parent;
end;
UseColumns := FHeader.UseColumns;
if UseColumns and FHeader.FColumns.IsValidColumn(FFocusedColumn) then
R := GetDisplayRect(Node, FFocusedColumn, not (toGridExtensions in FOptions.FMiscOptions))
else
R := GetDisplayRect(Node, NoColumn, not (toGridExtensions in FOptions.FMiscOptions));
// The returned rectangle can never be empty after the expand code above.
// 1) scroll vertically
if R.Top < 0 then // <==== what is the purpose of this if, I need always else part
begin
if Center then
SetOffsetY(FOffsetY - R.Top + ClientHeight div 2)
else
SetOffsetY(FOffsetY - R.Top);
ScrolledVertically := True;
end
else
if (R.Bottom > ClientHeight) or Center then
begin
HScrollBarVisible := (ScrollBarOptions.ScrollBars in [ssBoth, ssHorizontal]) and
(ScrollBarOptions.AlwaysVisible or (Integer(FRangeX) > ClientWidth));
if Center then
SetOffsetY(FOffsetY - R.Bottom + ClientHeight div 2)
else
SetOffsetY(FOffsetY - R.Bottom + ClientHeight);
// When scrolling up and the horizontal scroll appears because of the operation
// then we have to move up the node the horizontal scrollbar's height too
// in order to avoid that the scroll bar hides the node which we wanted to have in view.
if not UseColumns and not HScrollBarVisible and (Integer(FRangeX) > ClientWidth) then
SetOffsetY(FOffsetY - GetSystemMetrics(SM_CYHSCROLL));
ScrolledVertically := True;
end;
if Horizontally then
// 2) scroll horizontally
ScrolledHorizontally := ScrollIntoView(FFocusedColumn, Center);
end;
Result := ScrolledVertically or ScrolledHorizontally;
end;
I guess time to use new delphi features like class helpers :p
I wrote something simple in my main.pas, it seems working but I'm not sure it will cover all cases.
TBaseVirtualTreeHelper = class helper for TBaseVirtualTree
public
Procedure ScrollToTheBottom();
end;
{ TBaseVirtualTreeHelper }
procedure TBaseVirtualTreeHelper.ScrollToTheBottom;
Var
Node: PVirtualNode;
R: TRect;
begin
Node := Self.GetLast();
if Assigned(Node) and (Node <> Self.FRoot) then
begin
R := GetDisplayRect(Node, NoColumn, True);
if (R.Bottom > Self.ClientHeight) then
begin
Self.SetOffsetY(Self.FOffsetY - R.Bottom + Self.ClientHeight);
end;
end;
end;

Resources