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.
Related
I create over 100 rectangles at runtine in code below;
var
RectT: TRectangle;
MyThread: TThread;
Layout1: TLayout;
begin
MyThread := TThread.CreateAnonymousThread(procedure()
begin
TThread.Synchronize(nil, procedure()
var
z, i: integer;
begin
z := 0;
for i := 0 to 99 do
begin
RectT := TRectangle.Create(Self);
RectT.Name := 'Rectangle' + IntToStr(i);
RectT.Align := TAlignLayout.Top;
RectT.Margins.Top := 6;
RectT.Position.Y := z;
RectT.Height := 20;
RectT.Parent := Layout1;
if (i mod 10) = 0 then Layout1.UpdateEffects;
inc(z, 20);
end;
end);
end);
MyThread.FreeOnTerminate := True;
MyThread.Start;
end;
Why didn't display the rectangle when is created and only are displayed when finish the iteration of all rectangles?.
First, you need to move the for loop in one thread and the creation of the rectangles in a Synchronize call, as Deltics has done. The difference is that you do not need the call to Repaint and you need to use the currentthread to pass the call for synchronization.
Try this (in OnClick event of a Button):
procedure TForm4.Button1Click(Sender: TObject);
begin
TThread.CreateAnonymousThread(procedure
var
I,z: Integer;
Total: Integer;
begin
Total := 0;
for I := 1 to 99 do
begin
TThread.Synchronize (TThread.CurrentThread,
procedure
var
RectT: TRectangle;
begin
RectT := TRectangle.Create(Self);
RectT.Name := 'Rectangle' + IntToStr(i);
RectT.Align := TAlignLayout.Top;
RectT.Margins.Top := 6;
RectT.Position.Y := z;
RectT.Height := 20;
RectT.Parent := Layout1;
Inc(z, 20);
end);
end;
end).Start;
end;
If this code is running on the main thread (which appears to be the case since you don't mention any threading) then the first opportunity that the FMX runtime has of visually updating the UI is when your code has itself finished running.
If you want the UI to update to display the rectangles as they are added, then you will need to re-write this to use an approach that allows the UI an opportunity to repaint periodically.
UPDATE
Your updated code in the question now involves a thread. However, in your posted code you Synchronize() all of the work in that thread. Synchronized code runs in the main thread so the consequence of synchronizing all of the work is to remove any benefit of the thread at all.
You are nearly there however.
A small change to your posted code so that the layout child objects are added in the thread, synchronizing only the repainting of the layout object itself periodically, then you get the result you are seeking:
var
MyThread: TThread;
begin
MyThread := TThread.CreateAnonymousThread
(
procedure()
var
z, i: integer;
RectT: TRectangle;
begin
z := 0;
for i := 0 to 999 do
begin
RectT := TRectangle.Create(Self);
RectT.Name := 'Rectangle' + IntToStr(i);
RectT.Align := TAlignLayout.Top;
RectT.Margins.Top := 6;
RectT.Position.Y := z;
RectT.Height := 20;
RectT.Parent := Layout1;
TThread.Synchronize(nil, procedure()
begin
Layout1.Repaint;
end);
inc(z, 20);
end;
end
);
MyThread.FreeOnTerminate := True;
MyThread.Start;
end;
I have increased the number of child objects in this demonstration of the approach to 999 since 99 was not enough to see any appreciable change in performance.
As written, the above code also repaints after every rectangle has been added, but this could be easily modified in a way similar to your posted code so that the layout is repainted only after "batches" of rectangles have been added:
if (i mod 10) = 0 then
TThread.Synchronize(nil, procedure()
begin
Layout1.Repaint;
end);
This is a simplistic approach, addressing the immediate problem of updating the UI to show the progress of some background changes made to that UI using this very simple test case. Whether this is actually the most appropriate approach in your specific case only you can really say.
I have a TTreeView in Delphi, with nodes at three levels.
I use node data to store another label besides the node text.
Type
TNodeData = class
ExtraNodeLabel: WideString;
//... other members
end;
I have an OnAdvancedCustomDrawItem event, where i want to display this ExtraNodeLabel before the node text.
I wish to achieve this:
The blue text would be the extra label.
higlighted item: first two words are also an extra label
What i got so far, is this:
Problems:
For some reason i can't draw text with different style if i use DrawText/drawTextW (I need drawtextW because of unicode data)
The other problem is, that anything outside the dotted focus rectangle is unclickable
What needs to be solved:
How can i draw text with different style using DrawText/DrawtextW
How can i make the whole text clickable?
Code:
procedure TMainForm.TntTreeView1AdvancedCustomDrawItem(
Sender: TCustomTreeView; Node: TTreeNode; State: TCustomDrawState;
Stage: TCustomDrawStage; var PaintImages, DefaultDraw: Boolean);
var
txtrect, fullrect : TRect;
DC: HDC;
fs: integer;
fc: TColor;
ExtralabelRect: TRect;
nData: TNodeData;
begin
nData := nil;
if assigned(Node.Data) then begin
nData := TNodeData(Node.Data);
end;
DC := TntTreeView1.canvas.Handle;
txtRect := Node.DisplayRect(True);
fullrect := Node.DisplayRect(False);
if stage = cdPostPaint then begin
TntTreeView1.Canvas.FillRect(txtRect);
if (cdsFocused In State) And (cdsSelected in State) then begin
DrawFocusRect(DC,txtRect);
end;
txtRect.Left := txtRect.Left + 1;
txtRect.Top := txtRect.Top + 1;
txtRect.Right := txtRect.Right - 1;
txtRect.Bottom := txtRect.Bottom - 1;
ExtralabelRect := txtRect;
fs := TntTreeView1.Canvas.Font.size;
fc := TntTreeView1.Canvas.Font.Color;
if (nData <> nil) And (nData.ExtraNodeLabel <> '') then begin
TntTreeView1.Canvas.Font.Size := 7;
TntTreeView1.Canvas.Font.color := clBlue;
DrawTextW(
DC,
PWideChar(nData.ExtraNodeLabel),
Length(nData.ExtraNodeLabel),
ExtraLabelRect,
DT_LEFT or DT_CALCRECT or DT_VCENTER
);
DrawTextW(
DC,
PWideChar(nData.ExtraNodeLabel),
Length(nData.ExtraNodeLabel),
ExtraLabelRect,
DT_LEFT or DT_VCENTER
);
txtRect.right := txtRect.Right + ExtraLabelRect.Right + 5;
txtRect.Left := ExtraLabelRect.Right + 5;
end;
TntTreeView1.Canvas.Font.Size := fs;
TntTreeView1.Canvas.Font.color := fc;
DrawTextW(
DC,
PWideChar((Node as TTntTreeNode).Text),
-1,
txtRect,
DT_LEFT or DT_VCENTER
);
end;
end;
Solution by the OP
I managed to partially solve custom drawing, by defining a TFont variable, and using SelectObject and setTextColor. Setting font color and style works, but setting the font size doesn't.
var
nFont: TFont;
begin
DC := TntTreeView1.Canvas.Handle;
NFont := TFont.Create;
// rest of the code here ...
// i tried to set nFont.Size, but it doesn't seem to work
nFont.Size := 7;
nFont.Color := colorToRGB(clBlue);
nFont.Style := TntTreeview1.Font.Style + [fsBold];
SelectObject(DC,NFont.Handle);
SetTextColor(DC,colortoRGB(clBlue));
DrawTextW(
DC,
PWideChar(nData.nodeLabel),
Length(nData.nodeLabel),
ExtraLabelRect,
DT_LEFT or DT_VCENTER
);
// rest of the code here
end;
Source:
I used the idea from here
Update 2
I solved the second problem by setting the treeview's RowSelect property to true.
For this, to work, i had to set the ShowLines property to false, and custom draw the lines and the buttons. It works now.
Update 3
I improved the solution for the first problem, by not creating a new font, but selecting the canvas font for displaying text, and this way i was able to change any aspect of the font, and the system cleartype settings are also applied:
// set font size for the canvas font (font style can be set the same time)
TntTreeView1.Canvas.Font.Size := 7;
// select canvas font for DC
SelectObject(DC,TntTreeView1.Canvas.Font.Handle);
// set font color
SetTextColor(DC,colortoRGB(clBlue));
What would be the most simple and clean way to show a focused/selected listbox item with a Office XP style?
See this sample image to show the idea more clearer:
I think I need to set the Listbox Style to either lbOwnerDrawFixed or lbOwnerDrawVariable and then modify the OnDrawItem event?
This is where I am stuck, I am not really sure what code to write in there, so far I tried:
procedure TForm1.ListBox1DrawItem(Control: TWinControl; Index: Integer;
Rect: TRect; State: TOwnerDrawState);
begin
with (Control as TListBox).Canvas do
begin
if odSelected in State then
begin
Brush.Color := $00FCDDC0;
Pen.Color := $00FF9933;
FillRect(Rect);
end;
TextOut(Rect.Left, Rect.Top, TListBox(Control).Items[Index]);
end;
end;
I should of known that would not work, I get all kind of funky things going on:
What am I doing wrong, more importantly what do I need to change to make it work?
Thanks.
You forgot to paint the items for different states. You need to determine in what state the item currently is and according on that draw it.
What you have on your picture you can get this way. However this doesn't looks well if you have enabled multiselect and select more than one item:
procedure TForm1.ListBox1DrawItem(Control: TWinControl; Index: Integer;
Rect: TRect; State: TOwnerDrawState);
var
Offset: Integer;
begin
with (Control as TListBox) do
begin
Canvas.Font.Color := Font.Color;
if (odSelected in State) then
begin
Canvas.Pen.Color := $00FF9932;
Canvas.Brush.Color := $00FDDDC0;
end
else
begin
Canvas.Pen.Color := Color;
Canvas.Brush.Color := Color;
end;
Canvas.Rectangle(Rect);
Canvas.Brush.Style := bsClear;
Offset := (Rect.Bottom - Rect.Top - Canvas.TextHeight(Items[Index])) div 2;
Canvas.TextOut(Rect.Left + Offset + 2, Rect.Top + Offset, Items[Index]);
end;
end;
And the result with ItemHeight set to 16:
Bonus - continuous selection:
Here is a tricky solution implementing a continuous selection. The principle is to draw the item like before but then overdraw the item's border top and bottom lines with the lines of a color depending on selection state of the previous and next item. Except that, must be rendered also outside of the current item, since the item selection doesn't naturally invoke neighbour items to be repainted. Thus the horizontal lines are painted one pixel above and one pixel below the current item bounds (colors of these lines depends also on the relative selection states).
Quite strange here is the use of item objects to store the selected state of each item. I did that, because when using a drag & drop item selection, the Selected property doesn't return the real state until you release the mouse button. Fortunately, the OnDrawItem event of course fires with the real state, so as a workaround I've used storing of these states from the OnDrawItem event.
Important:
Notice, that I'm using the item objects to store the actual selection state, so be careful, and when you're using item objects for something else, store this actual states e.g. into an array of Boolean.
procedure TForm1.ListBox1DrawItem(Control: TWinControl; Index: Integer;
Rect: TRect; State: TOwnerDrawState);
const
SelBackColor = $00FDDDC0;
SelBorderColor = $00FF9932;
var
Offset: Integer;
ItemSelected: Boolean;
begin
with (Control as TListBox) do
begin
Items.Objects[Index] := TObject((odSelected in State));
if (odSelected in State) then
begin
Canvas.Pen.Color := SelBorderColor;
Canvas.Brush.Color := SelBackColor;
Canvas.Rectangle(Rect);
end
else
begin
Canvas.Pen.Color := Color;
Canvas.Brush.Color := Color;
Canvas.Rectangle(Rect);
end;
if MultiSelect then
begin
if (Index > 0) then
begin
ItemSelected := Boolean(ListBox1.Items.Objects[Index - 1]);
if ItemSelected then
begin
if (odSelected in State) then
begin
Canvas.Pen.Color := SelBackColor;
Canvas.MoveTo(Rect.Left + 1, Rect.Top);
Canvas.LineTo(Rect.Right - 1, Rect.Top);
end
else
Canvas.Pen.Color := SelBorderColor;
end
else
Canvas.Pen.Color := Color;
Canvas.MoveTo(Rect.Left + 1, Rect.Top - 1);
Canvas.LineTo(Rect.Right - 1, Rect.Top - 1);
end;
if (Index < Items.Count - 1) then
begin
ItemSelected := Boolean(ListBox1.Items.Objects[Index + 1]);
if ItemSelected then
begin
if (odSelected in State) then
begin
Canvas.Pen.Color := SelBackColor;
Canvas.MoveTo(Rect.Left + 1, Rect.Bottom - 1);
Canvas.LineTo(Rect.Right - 1, Rect.Bottom - 1);
end
else
Canvas.Pen.Color := SelBorderColor;
end
else
Canvas.Pen.Color := Color;
Canvas.MoveTo(Rect.Left + 1, Rect.Bottom);
Canvas.LineTo(Rect.Right - 1, Rect.Bottom);
end;
end;
Offset := (Rect.Bottom - Rect.Top - Canvas.TextHeight(Items[Index])) div 2;
Canvas.Brush.Style := bsClear;
Canvas.Font.Color := Font.Color;
Canvas.TextOut(Rect.Left + Offset + 2, Rect.Top + Offset, Items[Index]);
end;
end;
And the result:
You need to look at the value of the State variable that is passed into the function. This tells you if the item is selected or not and you can then set the brush and pen appropriately.
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;
Ok, so I'm pretty new to Delphi (as you'll see from my code - try not to laugh too hard and hurt yourselves), but I've managed to make a little desktop canvas color picker. It works, kinda, and that's why I'm here :D
It seems to be leaking. It starts off using about 2 MB of memory, and climbs up about 2 kB per second until it reaches about 10 MB after 10 minutes or so. On my dual core 2.7 ghz cpu, it's using anywhere from 5% to 20% cpu power, fluctuating. My computer became unresponsive after running it for about 10 minutes without stopping the timer.
You can see in the source code below that I am freeing the TBitmap (or trying to, not sure if it's doing it, doesn't seem to be working).
procedure TForm1.Timer1Timer(Sender: TObject);
begin
GetCursorPos(MousePos);
try
Canvas1 := TCanvas.Create;
Canvas1.Handle := GetDC(0);
Pxl := TBitmap.Create;
Pxl.Width := 106;
Pxl.Height := 106;
W := Pxl.Width;
H := Pxl.Height;
T := (W div 2);
L := (H div 2);
Zoom := 10;
Rect1 := Rect(MousePos.X - (W div Zoom), MousePos.Y - (H div Zoom), MousePos.X + (W div Zoom), MousePos.Y + (H div Zoom));
Rect2 := Rect(0, 0, H, W);
Pxl.Canvas.CopyRect(Rect2, Canvas1, Rect1);
Pxl.Canvas.Pen.Color := clRed;
Pxl.Canvas.MoveTo(T, 0);
Pxl.Canvas.LineTo(L, H);
Pxl.Canvas.MoveTo(0, T);
Pxl.Canvas.LineTo(W, L);
Image1.Picture.Bitmap := Pxl;
finally
Pxl.Free;
end;
try
Pxl2 := TBitmap.Create;
Pxl2.Width := 1;
Pxl2.Height := 1;
Box1 := MousePos.X;
Box2 := MousePos.Y;
BitBlt(Pxl2.Canvas.Handle, 0, 0, 1, 1, GetDC(0), Box1, Box2, SRCCOPY);
C := Pxl2.Canvas.Pixels[0, 0];
Coord.Text := IntToStr(Box1) + ', ' + IntToStr(Box2);
DelColor.Text := ColorToString(C);
HexColor.Text := IntToHex(GetRValue(C), 2) + IntToHex(GetGValue(C), 2) + IntToHex(GetBValue(C), 2);
RGB.Text := IntToStr(GetRValue(C)) + ', ' + IntToStr(GetGValue(C)) + ', ' + IntToStr(GetBValue(C));
Panel1.Color := C;
finally
Pxl2.Free;
end;
end;
procedure TForm1.OnKeyDown(Sender: TObject; var Key: Char);
begin
if Key = #13 then
begin
if Timer1.Enabled then
begin
Timer1.Enabled := false;
Panel2.Caption := 'Got it! Press Enter to reset.';
end
else
begin
Timer1.Enabled := true;
Panel2.Caption := 'Press Enter to lock color.';
end;
end;
end;
Note: The timer is set to run every 10 ms, if that makes any difference.
ANY and all help figuring out why this is leaking and using so much resources would be greatly appreciated!
You can nab the project here if you want it (Delphi 2010): http://www.mediafire.com/file/cgltcy9c2s80f74/Color%20Picker.rar
Thanks!
You never free your Canvas1 object, leaking both process heap and GDI obj. handles.
As user said above, TCanvas instance which owns DC of desktop window never freed, not releasing DC. I found another DC leak here:
BitBlt(Pxl2.Canvas.Handle, 0, 0, 1, 1, GetDC(0), Box1, Box2, SRCCOPY);
^^^^^^^^
This not solves memory leak but explains why Windows becomes unresponsive after 20 minutes (assuming previous issue has been patched already)
Every GetDC call requires ReleaseDC counter-part. GDI objects in the fact are even more precious than memory.
Ok, I found the solution (finally) after tinkering around with it a bit and following a few of the pointers on here. No one really hit it right on the head, but everyone was on the right track. The problem was that I was calling GetDC() inside the FUNCTION (and in earlier versions the timer procedure as well). Moving it outside of "try ... finally" while keeping it in the function (as suggested) still didn't yield results, but it was getting close and gave me the idea that actually worked. So I moved it a bit further away - into the Form's OnCreate event.
Here's the final code:
function DesktopColor(const X, Y: Integer): TColor;
begin
Color1 := TCanvas.Create;
Color1.Handle := DC;
Result := GetPixel(Color1.Handle, X, Y);
Color1.Free;
end;
procedure TForm1.Timer1Timer(Sender: TObject);
begin
GetCursorPos(Pos);
Rect1 := Rect(Pos.X - (W div Zoom), Pos.Y - (H div Zoom), Pos.X + (W div Zoom), Pos.Y + (H div Zoom));
Rect2 := Rect(0, 0, H, W);
Pxl.Canvas.CopyRect(Rect2, Canvas1, Rect1);
Pxl.Canvas.Pen.Color := clRed;
Pxl.Canvas.MoveTo(T, 0);
Pxl.Canvas.LineTo(L, H);
Pxl.Canvas.MoveTo(0, T);
Pxl.Canvas.LineTo(W, L);
Image1.Picture.Bitmap := Pxl;
Coord.Text := IntToStr(Pos.X) + ', ' + IntToStr(Pos.Y);
C := DesktopColor(Pos.X, Pos.Y);
DelColor.Text := ColorToString(C);
HexColor.Text := IntToHex(GetRValue(C), 2) + IntToHex(GetGValue(C), 2) + IntToHex(GetBValue(C), 2);
RGB.Text := IntToStr(GetRValue(C)) + ', ' + IntToStr(GetGValue(C)) + ', ' + IntToStr(GetBValue(C));
Panel1.Color := C;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
Pxl := TBitmap.Create;
Canvas1 := TCanvas.Create;
DC := GetDC(0);
Pxl.Width := 106;
Pxl.Height := 106;
Canvas1.Handle := DC;
W := Pxl.Width;
H := Pxl.Height;
T := (W div 2);
L := (H div 2);
Zoom := 10;
Timer1.Enabled := True;
end;
procedure TForm1.OnKeyDown(Sender: TObject; var Key: Char);
begin
if Key = #13 then
begin
if Timer1.Enabled then
begin
Timer1.Enabled := false;
Panel2.Caption := 'Got it! Press Enter to reset.';
end
else
begin
Timer1.Enabled := true;
Panel2.Caption := 'Press Enter to lock color.';
end;
end;
end;
procedure TForm1.OnDestroy(Sender: TObject);
begin
ReleaseDC(0, Canvas1.Handle);
ReleaseDC(0, Color1.Handle);
end;
And the final tally: drumroll CPU usage: 00% idle, 01% spikes if you move the mouse fast enough; Memory usage: ~3,500 kB solid, remaining unchanged. I even bumped the timer up from 10 ms to 5 ms and still get the same numbers.
Here's the final project with all the aforementioned fixes: http://www.mediafire.com/file/ebc8b4hzre7q6r5/Color%20Picker.rar
Thanks to everyone who helped, I greatly do appreciate it! I'm going to go ahead and open source the project for everyone who stumbles across this post and finds it useful. No license, do with it whatever you will. No credit necessary, but if you want to leave my name in there, that would be cool :D
Some comments on your code in DesktopColor
If the creation or GetDC fails, no resource will be locked and the unlock or free will generate an error, because you are trying to free a resource that does not exist.
The rule is that initialization should always be done before the try, because otherwise you will not know whether is is safe to deconstruct the entry.
In this case it's not a huge issue because GetxDC/ReleaseDC does not generate exceptions, it just gives back a 0 if unsuccesful.
Secondly I recommend putting in tests to make sure that your calls using DC's are succesful. When using Delphi objects you don't need that because the exceptions will take care of that, but Windows DC do not use exceptions, so you'll have to do your own testing. I recommend using assertions, because you can enable then in debug time and disable them when the program is debugged.
But because GetxDC never generates exceptions and to be consistent I'd recommend changing the code into:
{$C+} //enable assertions for debug purposes.
//or {$C-} //Disable assertions in production code
function DesktopColor(const X, Y: Integer): TColor;
var
Color: TCanvas;
Handle: THandle;
begin
Color := TCanvas.Create;
//If the create fails GetWindowsDC will not get stored anywhere
//and we cannot free it.
Handle:= GetWindowDC(GetDesktopWindow);
try
Assert(Handle <> 0);
Color.Handle := Handle; //Will generate an exception if create failed.
Handle := 0;
Result := GetPixel(Color.Handle, X, Y);
finally
//Free the handle if it wasn't transfered to the canvas.
if Handle <> 0 then ReleaseDC(0, Handle);
Color.Free; //TCanvas.Destroy will call releaseDC on Color.handle.
//If the transfer was succesful
end; {tryf}
end;
The same arguments apply to Timer1Timer.
Warning
When you disable assertions Delphi will remove the entire assert statement from your project, so don't put any code with side effects into an assert!
Links:
Assertions: http://beensoft.blogspot.com/2008/02/using-assert.html