Learning Delphi (have a ways to go), using Rio.
I figured out how to use a colored background in TStringGrid rows - but it looks like I need to refresh when data in those rows changes (so as to get different colors to show up based on the data changes).
I thought that just setting the cell values to their then-existing values would cause the refresh. But it didn't. I could tell for sure that it didn't - because I had a debug breakpoint placed within the StringGrid1DrawCell procedure - and that breakpoint was not hit.
The code that I had been using to hopefully cause the refresh in TStringGrid was as follows (note: S is defined as a String):
S := StringGrid1.Cells[1, i];
StringGrid1.Cells[1, i] := S;
Is the basic assumption (that just setting/resetting values of the cell contents causes a refresh) in error?
If the idea is right, but the method is wrong: could you let me know what to do differently?
The OnDrawCell event is fired only when a given cell needs to be painted onscreen.
Setting a specific cell's value will invalidate only that cell, thus triggering a repaint of only that cell, not the cell's entire row, or the grid as a whole.
If you need to trigger a repaint of an entire row, call the grid's (protected) InvalidateRow() method, eg:
type
TStringGridAccess = class(TStringGrid)
end;
procedure TMyForm.DoSomething;
begin
...
StringGrid1.Cells[1, i] := ...;
TStringGridAccess(StringGrid1).InvalidateRow(i);
...
end;
If you need to trigger a repaint of the entire grid, call the grid's (public) Invalidate() method, eg:
StringGrid1.Cells[1, i] := ...;
StringGrid1.Invalidate;
Related
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.
I have a frame that is being placed on a form. I expect to be placing a number of instances of this frame on the form.
It has a drawgrid with 2 columns and in the OnResize event I make the 2nd column extend to the end of the available space. That works when the form is manually resized with the frame Align set to alTop. But when the form first appears even though FrameResize gets called it has no effect. (Though it did have the desired effect when I put a break point on it).
So, what I am doing now is calling the FrameResize from the forms OnShow handler, but that is ugly. The frame should be able to appear correctly formed without help from the Form.
Any ideas? I did try overriding SetParent, but that didn't work. Using Xe2.
TIA
Mark
I have solved it with advice from Peter Below, Delphi Team B Delphi member.
I overrode the frame's set bounds. It was getting called even before the component variables were set, so it looks like this
procedure TfaDupDisplay.SetBounds(ALeft, ATop, AWidth, AHeight: Integer); // Had to use SetBounds because OnRezise was not working
var grid: TDrawGrid;
begin
inherited;
if pnlWebData = nil then
exit;
pnlWebData.Width := Width div 2;
for grid in TArray<TDrawGrid>.Create(grdData, grdDup) do
grid.ColWidths[1] := grid.Width - grdData.ColWidths[0];
end{ SetBounds};
This happens in all Delphi up to XE3:
Create a form and put a panel on it. Anchor the panel to [akLeft, akTop, akRight, akBottom], but leave space between it and the borders.
Add a button which calls RecreateWnd()
Run the app. Resize the form so that the panel is hidden because it's less than 0 pixels in size due to anchoring. Press the RecreateWnd button.
Resize the form back and note that the panel's anchoring is broken.
As long as I can remember myself using Delphi, anchors were always impossible to use because of this. Resize the form, then dock it: the window is recreated, your layout is broken.
I wonder if there's some sort of workaround?
Update
Two workarounds are available in the comments, one proven and stable but with form blinking, another one experimental but potentially more thorough and clean.
I'm not going to vote for either one for a while, as one of those is mine and I'm not even sure it is stable. Instead, I'll wait for some public input.
The two options I have used of which neither is really ideal for problems with the bottom and right anchors are:
Make the window big again before calling or causing to be called RecreateWnd();, then make it small again. Has to be visible before you make it small again however.
Set the Form's constraints so it can't be re-sized so small that stuff ends up hidden.
An example that flashes the larger form, use height and width large values enough so the panel is not hidden:
procedure TForm1.Button1Click(Sender: TObject);
Var
OldWidth, OldHeight : integer;
begin
OldWidth := Form1.Width;
OldHeight := Form1.Height;
Form1.Visible := false;
Form1.Width := 1000;
Form1.Height := 800;
RecreateWnd();
Form1.Visible := true;
Form1.Width := OldWidth;
Form1.Height := OldHeight;
end;
Turns out that the function which breaks everything is UpdateAnchorRules. TControl stores FOriginalParentSize and it's own original size in FAnchorRules, and uses that to auto-resize as parent resizes. UpdateAnchorRules() takes current parent size and current control Width and Height and saves those into FOriginalParentSize and FAnchorRules.
If everything worked properly that would have no effect during normal resizes, as the control and it's parent change size in accord.
But when the control Width is less than zero due to anchoring, Windows, and consequently Delphi still considers it 0. If UpdateAnchorRules is called at that point, it saves wrong, out-of-accord 0 value for original width. After this the layout is beyond repair.
(If it's not called, Width continues to be updated in proper relation to parent Width due to original sizes preserved)
Turns out anything which involves creating a window handle calls UpdateAnchorRules twice: first in WinAPI CreateWindow as it dispatches WM_SIZE before returning (and WM_SIZE handler calls UpdateAnchorRules), and second, explicitly in CreateHandle after handle creation.
It would seem that as long as we can disable UpdateAnchorRules for the duration of CreateHandle, we will succeed. But there are explicit calls to UpdateAnchorRules in CreateHandle, which means someone thought there needs to be an adjustment of Anchor rules after handle creation.
So perhaps I'm missing something, and by disabling it something will break?
Anyway, there are two ready ways to disable UpdateAnchorRules: to set FAnchorMove or to set csLoading. First one is no good as there's code which clears it midway through RecreateWnd and then calls UpdateAnchorRules again.
Second one works and here's a solution:
type
TComponentHack = class helper for TComponent
public
procedure SetCsLoading(Value: boolean);
end;
procedure TComponentHack.SetCsLoading(Value: boolean);
var i: integer;
begin
if Value then
Self.FComponentState := Self.FComponentState + [csLoading]
else
Self.FComponentState := Self.FComponentState - [csLoading];
for i := 0 to Self.ComponentCount-1 do
if Self.Components[i] is TControl then
TControl(Self.Components[i]).SetCsLoading(Value);
end;
procedure SafeRecreateWnd();
begin
MyControl.SetCsLoading(true);
try
MyControl.RecreateWnd(); //or any operation which triggers it -- such as docking or making the window visible first time after RecreateWnd()
finally
MyControl.SetCsLoading(false);
end;
end;
Disclaimer:
I have no idea what else will be broken by running TControl operations with csLoading set.
Better alternative would be to hook UpdateAnchorRules procedure and add another flag check specifically for this purpose, but that'd require either reimplementing UpdateAnchorRules completely (prone to breaking on different versions of Delphi with different original UpdateAnchorRules) or inventing some way to call original UpdateAnchorRules which is usually destroyed by rewriting it with a hook.
I have a TGrid with a mixture of columns (ImageColumn and StringColumn). I can populate it using onGetValue event which works fine. My questions are:
How to force the entire grid to rebuild and cause onGetValue event?
I'm using UpdateStyle at the monent.
How can I update a single cell in the grid?
The grid updates only visible cells! Grid1.UpdateStyle force the grid to rebuild and is causing onGetValue events but its slow. Grid1.ReAlign is much faster.
As soon as cells become visible, they will be updated.
Updating 1 cell:
procedure TForm1.UpdateCell(col, row: integer);
var
cell: TStyledControl;
begin
cell := Grid1.Columns[col].CellControlByRow(row);
if Assigned(cell) then
cell.Data := 'Note: use the same datasource as OnGetValue';
end;
cell is not assigned when row never become visible.
The other option is to call Grid1.beginUpdate; make your changes and then call Grid1.endupdate; which will cause the visible grid to recalculate and redraw.
How to automatically resize TStringGrid row (DefaultRowHeight) to match the height of the font used?
I do something like
Grid.DefaultRowHeight:= Grid.Canvas.TextHeight('X') + 4;
but it is not working. For small fonts, the height of the rows is way too big.
Update:
It seems to be actually a problem with program's logic. If I change the font multiple times, the current height of the row matches the size of the font from previous font change event (it is one step behind).
I use this code to intercept font's size change:
procedure TStrGrid.CMFontChanged(var Message: TMessage);
begin
inherited; // let TControl react first
DefaultRowHeight:= Canvas.TextHeight('ApApM')+ 4;
end;
It acts as if the procedure will be:
begin
DefaultRowHeight:= Canvas.TextHeight('ApApM')+ 4;
inherited;
end;
(like first it changes the height, then it actually sets the correct font size - therefore the height is one step behind)
It has nothing to do with this order: both the inherited CMFontChanged message handler and the DefaultRowHeight property setter call invalidate. It is due to the current font setting which is not yet updated:
procedure TStrGrid.CMFontchanged(var Message: TMessage);
begin
Canvas.Font := Font;
DefaultRowHeight := Canvas.TextHeight('Ap') + GridLineWidth + 3;
end;
Explanation:
Invalidate only flags windows to repaint the window (the grid) somewhere in future. That happens certainly after obtaining the text height. As alternative you can call Repaint prior to the request for the new text height, but that would result in a double repaint, hence setting Canvas.Font.
When it comes to event ordering related issues then the easiest and quickest solution often was posting yourself a message via PostMessage (here from your CMFontChanged handler) and doing the update when receiving the posted message.
This has the advantage that it doesn't interfer with whatever the grids need to do to update its inner state.