FM2 Object Repaint Issue - delphi

Good evening guys.
I'm currently designing a social networking client for Twitter and Facebook in Firemonkey FM2 (delphi) and I'm experiencing a frustrating issue. At present, I've only got the Twitter code in process, but the issue is related to the [re]drawing of visual objects.
I've created a custom-styled TListboxItem layout in a stylebook consisting of multiple child components such as TText, TButton, and TImage. I've already dealt with connecting to Twitter and retrieving feed details. Each item retrieved is added to a TListbox and styled using my custom ListboxItem style layout.
Now, the issue is related to updating information on items in the list that aren't visible. For example, the items that are visible in the list without scrolling show their information correctly. Those that aren't visible besides the final item in the list have several of their details not set/visible. When I scroll the list downwards, and then back up, there's often 1 of the items that was originally visible will now be missing it's information.
To explain this a little more, i've got a TImage (known as photo) which is used to show the photo of the person who posted the 'tweet'. I've got the standard TText (known as text) used to show the contents/text of the tweet itself. I've got 2 buttons (known as Like and Share) used to perform their respective functions. I've then finally got another TText (known as NameDate) used to show the name of the tweeter and the date the tweet was posted.
I'm using this code to create the object and modify the data it shows;
for i := 0 to TwitObj.Statuses.Count-1 do
begin
FeedItem := TListBoxItem.Create(LBFeed);
FeedItem.Parent := LBFeed;
FeedItem.StyleLookup := 'FeedItem';
FeedItem.WordWrap := True;
FeedItem.StyledSettings := [TStyledSetting.ssFamily, TStyledSetting.ssSize, TStyledSetting.ssStyle, TStyledSetting.ssFontColor, TStyledSetting.ssOther];
NameDate := Feeditem.FindStyleResource('txtnamedate') as TText;
Photo := FeedItem.FindStyleResource('photo') as TImage;
Like := FeedItem.FindStyleResource('btnlike') as TButton;
Share := FeedItem.FindStyleResource('btnshare') as TButton;
Share.Text := 'Retweet';
Like.Text := 'Favorite';
NameDate.Text := Twitobj.Statuses.Items[i].User.Name +
'(#'+TwitObj.Statuses.Items[i].User.ScreenName+
') - '+DateTimeToStr(TwitObj.Statuses.Items[i].CreatedAt);
FeedItem.Text := TwitObj.Statuses.Items[i].Text;
begin
if DirectoryExists('imagecache\') = false then CreateDir('imagecache\');
if FileExists('imagecache\'+TwitObj.Statuses.Items[i].User.ScreenName+'.jpg') = False then
begin
try
rcv := TMemoryStream.Create;
GtPhoto.URL := TwitObj.Statuses.Items[i].User.ImageURL;
GtPhoto.RcvdStream := rcv;
GtPhoto.Get;
rcv.SaveToFile('imagecache\'+TwitObj.Statuses.Items[i].User.ScreenName+'.jpg');
finally
Rcv.Free;
end;
end;
end;
Photo.Bitmap.LoadFromFile('imagecache\'+TwitObj.Statuses.Items[i].User.ScreenName+'.jpg');
GTPhoto is a standard ICS HTTP Client component, while TwitObj is my Twitter component. You can see that I'm saving the photo to a directory instead of streaming it. This was merely to check whether it was an issue with streams, but it's probably advisable to used a cache of some sort anyway.
The images download correctly, and the information for the relevant StyleResources in the custom ListBoxItem layout is updated as expected, but only for items that are visible without scrolling. If I scroll down the list, only the Text of each item is correct, while the other resources which were set at runtime have returned to the way they're designed in the stylebook (i.e. blank text, image, etc).
Am I missing something here? I understand the design intents of Bitmaps were changed in XE3 for the sake of performance, but surely Embarcadero wouldn't have overlooked something like this. Surely it's not expected for us to create each item inside the parent at runtime (and thus dealing with alignments and such) instead of using a stylebook resource, is it?
Any assistance or insight would be greatly appreciated.

FireMonkey can load and unload the style for a control at any moment. It was rather lax with this in FM1, but under FM2, styling elements are removed when a control is not visible and reapplied when it becomes visible again (in order to conserve memory, mainly in preparation for Mobile Studio).
What you need to do is override the ApplyStyle method. In it look up and set data in your style elements. This will probably mean that your control(s) need to cache what will be passed to the style.
Also note that if you are caching references to style elements (i.e. what you get back from FindStyleResource) then these will be freed when the style is unloaded and your pointers will be invalid. If so, you need to override FreeStyle and nil any pointers you may have cached.

Related

What has happened to ComboBox.Sorted := True; in Delphi 10.2?

Having recently received a 'Tumbleweed' badge for my last question, I am not sure whether I should be asking any more questions, but here goes.
I am populating a TComboBox with items from a sqlite table and this works fine. In my previous version of Delphi I was able to use ComboBox1.Sorted := True; to sort the items, but this seems to have disappeared in Delphi 10.2. I can sort the items in the table by applying a query and then populate the TComboBox from the sorted table. However, for curiosities sake I would like to find out how one now sorts items in a TComboBox. I have found some references to TComboBox(Sort:Compare) but have not succeeded in getting this to work to as of yet.
Can somebody please shed some light on this - many thanks
In Firemonkey you can populate a TComboBox instance either simply with the Items property of type TStrings or you add TListBoxItem instances with the form designer. But internally always TListBoxItem for the elements is used.
To use the TComboBox.Sort you need to provide an anonymous compare-function.
This is a simple example usage of TComboBox.Sort
cbxItems.Sort(
function (pLeft, pRight: TFMXObject): Integer
var
lLeft, lRight: TListBoxItem;
begin
lLeft := TListBoxItem(pLeft);
lRight := TListBoxItem(pRight);
Result := String.Compare(lLeft.Text, lRight.Text);
end
);

Cannot Destroy Dynamically created Menu Item in Delphi

Firstly, yes I have looked all over the net and still cannot seem to destroy dynamically created menu items. Using Delphi XE. I create the items thus (for the purposes of the exercise SubMenuName is 'Test1':
MenuItemCreated := TMenuItem.Create(PopupMenu1);
MenuItemCreated.Caption:= SubMenuCaption
MenuItemCreated.Hint := SubMenuHint;
MenuItemCreated.Name := SubMenuName;
MenuItemCreated.OnClick := SubMenuClick;
MenuItemCreated.AutoHotkeys := maManual;
MySubMenu.Add(MenuItemCreated);
There is no issue using the sub-menu(s) created. The procedure SubMenuClick works as it should, and I identify the correct subMenu item so no issues there. What I then do is an application logout which is supposed to free the dynamically created sub-menus using this code (although I have tried many variations):
// Get rid of the menu items created
While MySubMenu.Count > 0 do
begin
Itemtodelete := MySubMenu.Items[0];
FreeandNil(ItemtoDelete);
end;
I have put in showmessage() debug lines that show the component names of the menu items being freeandnil'd and they are what I'd expect, ie. 'Test1' and any others I've created. I then log back in to my application (which was still running, but with me logged out). The software then tries to recreate the same sub menus with the same names (as nothing has changed as far as my application is concerned and they were previously disposed of (supposedly)). I immediately get the exception raised:
Error: A component Named Test1 already exists
I am at a complete loss as to how to dispose of the submenu items so that I can recreate them later with the same names.
Any help greatly appreciated.
Thanks,
KB
You did not say it, so I have to assume that MySubMenu is a MenuItem of PopupMenu1. If not please clarify.
To delete items from MySubMenu in order to recreate them again later, it's easyest to call the Clear method:
procedure TForm5.Button2Click(Sender: TObject);
begin
MySubMenu.Clear;
end;
which deletes all menu items of MySubMenu and frees their memory.
In order to recreate the items later, you can not use Delete() or Remove(), without also freeing the memory because they do not free the memory of the items. This is documented in help:
http://docwiki.embarcadero.com/Libraries/XE7/en/Vcl.Menus.TMenuItem.Delete
http://docwiki.embarcadero.com/Libraries/XE7/en/Vcl.Menus.TMenuItem.Remove
With these methods you must free the memory yourself, before you recreate the menu items. But then, it's not necessary to even call Delete or Remove, you can just simply Free the items:
procedure TForm5.Button2Click(Sender: TObject);
var
mi: TMenuItem;
begin
while MySubMenu.Count > 0 do
begin
mi := MySubMenu.Items[0];
mi.Free;
end;
end;
There's no need to call FreeAndNil.
This last option looks very much as yours, with which you had problems when recreating the menu items. I can't reproduce the error except when using Delete() or Remove() without freeing.
Since the Popup menu owns the items, you do not Free it. Instead of FreeAndNil use MySubMenu.Delete(0) OR more appropriately MySubMenu.Items.Clear instead of the entire While routine.
On App shutdown the popup menu will clear it, there's no need to do it manually unless you're rebuilding the menu.

How to stop the sort symbol from hiding when TVirtualTreeView (TVirtualStringTree) header is double clicked

I am using a VirtualStringTree control as a list view and using the sort features. However when I double click the VirtualStringTree header the sort direction symbol hides until I click the header again.
Can that behaviour be disabled?
Things that I have tried but do not work:
I have searched the properties and cannot find a related setting
I have linked the double click header event to the click header event
My environment is Delphi 2007 Pro, Windows 7 Pro 64bit.
I had the same issue with double-click and hiding of sorting triangle and instead I just wanted a simple toggle up/down with nothing else. This issue is present unfortunately in latest VirtualTreeView (4.8.7) as well.
Here is a bit of code that fixes the issue - put something like this in your OnHeaderClick event (not OnHeaderDblClick !).
The relevant line is if HitInfo.Column = NoColumn then Exit; which fixes the double-click problem. You may or may not use the rest of code for your own purposes but it may be useful to someone else. The rest of explanation is in the code comments.
You don't need to define OnHeaderDblClick event - it may be empty if not needed so you may want to remove that from your code.
UPDATE
Also read comments from TLama as it seems that with version 5.0.0. this fix may not operate as intended. With the current version it does though.
{**
A column header of a VirtualStringTree was clicked: Toggle the sort direction
}
procedure TMainForm.vstHeaderClick(Sender: TVTHeader; HitInfo: TVTHeaderHitInfo);
begin
// Don't call sorting procedure on right click
// Some list-headers have a contextmenu which should popup then.
if HitInfo.Button = mbRight then Exit;
// Beginning with VT's r181, this proc is also called when doubleclicking-to-autofit
// Seems buggy in VT as this suddenly calls it with Column=-1 in those cases.
// See also issue #1150
if HitInfo.Column = NoColumn then Exit;
if Sender.SortColumn <> HitInfo.Column then Sender.SortColumn := HitInfo.Column
else if Sender.SortDirection = sdAscending then Sender.SortDirection := sdDescending
else Sender.SortDirection := sdAscending;
Sender.Treeview.SortTree( HitInfo.Column, Sender.SortDirection );
end;

Trouble with detecting mouse movements in a TcxGrid when dragging files from Windows Explorer

I recently added a feature to a large application written in Delphi (version 2009) that allows the user to drag files from Windows explorer and drop them on a TcxGrid control. I achieved this via the common method of attaching my own window proc to the grid and intercepting the WM_DROPFILES message:
originalGridWindowProc := cxGrid.WindowProc; // remember the old one
cxGrid.WindowProc := GridWindowProc; // assign the new one
DragAcceptFiles(cxGrid.Handle, LongBool(True)); // setup to accept dropped files
I now am trying to enhance this feature to detect when the user drops a file onto an existing row in the grid, which will begin the process of overwriting an existing file with a new version.
My first thought was to see if the grid control's mouseover event would work. It does, but not during the drag operation.
I then used a program called Winspector to see what messages were being sent to the grid control as the mouse is moved over the grid, and I can now detect what row the mouse is over and highlight it. I'm using the same technique as above, but in this case I am overriding the window proc for the GridSite and not the grid itself, because that is where the messages appear to be going according to Winspector:
originalGridSiteWindowProc := cxGrid.ActiveView.Site.WindowProc;
cxGrid.ActiveView.Site.WindowProc := GridSiteWindowProc;
Here is the body of GridSiteWindowProc:
procedure Tfrm.GridSiteWindowProc(var message: TMessage);
var
hitTest: TcxCustomGridHitTest;
gridRecord: TcxCustomGridRecord;
begin
//Log(IntToStr(message.Msg));
case message.Msg of
WM_NCHITTEST: begin
hitTest := cxGrid.ActiveView.GetHitTest(cxGrid.ScreenToClient(Mouse.CursorPos));
if hitTest is TcxGridRecordCellHitTest then begin
gridRecord := TcxGridRecordCellHitTest(HitTest).GridRecord;
if Not gridRecord.Focused then
gridRecord.Focused := True;
end;
originalGridSiteWindowProc(message);
end
else
originalGridSiteWindowProc(message);
end;
end;
As you can see, I'm trapping the WM_NCHITTEST message to achieve this. According to Winspector, this message also gets sent to the grid site window during the drag operation, but if I uncomment that Log() statement which will output the message value to a string list (which I manually dump to a memo field afterwards), I have determined that for some reason, I only get one or two of these messages when dragging a file over the grid.
Now - here's the interesting part: if I have Winspector running and monitoring messages going to that window, I suddenly start getting all the WM_NCHITTEST messages during the file drag operation. This is also the case if I output the integer value of all the messages coming to the window proc directly to a separate log window instead of to a string list buffer first. I am hoping that someone will be able to offer some clue as to why this is happening or how to get this to work.
Rather than using the WM_DROPFILES message, you should use OLE Drag'n'Drop. Look at the RegisterDropTarget API. You can get more detailed information about where a drag or drop is taking place. You can also accept more kinds of drag objects.

Should I use delphi tframes for multi-pages forms?

I have some forms in my application which have different "states" depending on what the user is doing; for exemple when listing through his files the form displays some data about that file in a grid, but if he clicks on some button the grid is replaced by a graph relating to it. Simply said, the controls in the form depends on the what the user wants to do.
Of course the obvious way of doing that was showing/hidding controls as needed, works like a charm for small numbers but once you reach 10/15+ controls per state (or more than 3 states really) it's unusable.
I'm experimenting with TFrames right now: I create a frame for every state, I then create an instance of each frame on my form on top of each other and then I only display the one I want using Visible - while having some controls on top of it, out of any frame since they all share them.
Is this the right way to do what I want, or did I miss something along the way ? I thought I could create only one tframe instance and then chose which one to display in it but it doesn't look that way.
Thanks
Looks like Frames are an excellent choice for this scenario. I'd like to add that you can use a Base Frame and Visual Inheritance to create a common interface.
And to the second part: You design a Frame like a Form but you use it like a Control, very few restrictions. Note that you could just as easily use Create/Free instead of Show/Hide. What is better depends on how resource-heavy they are.
There's a better way to handle dealing with the frames that won't take up nearly as much memory. Dynamically creating the frames can be a very elegant solution. Here's how I've done it in the past.
On the form, add a property and a setter that handles the placement of it on the form:
TMyForm = class(TForm)
private
FCurrentFrame : TFrame;
procedure SetCurrentFrame(Value : TFrame);
public
property CurrentFrame : TFrame read FCurrentFrame write SetCurrentFrame;
end;
procedure TMyForm.SetCurrentFrame(Value : TFrame)
begin
if Value <> FCurrentFrame then
begin
if assigned(FCurrentFrame) then
FreeAndNil(FCurrentFrame);
FCurrentFrame := Value;
if assigned(FCurrentFrame) then
begin
FCurrentFrame.Parent := Self; // Or, say a TPanel or other container!
FCurrentFrame.Align := alClient;
end;
end;
end;
Then, to use it, you simply set the property to a created instance of the frame, for example in the OnCreate event:
MyFrame1.CurrentFrame := TSomeFrame.Create(nil);
If you want to get rid of the frame, simply assign nil to the CurrentFrame property:
MYFrame1.CurrentFrame := nil;
It works extremely well.
I have a word for you : TFrameStack. Simply what the name suggests.
It has a few methods: PushFrame(AFrame), PopFrame, PopToTop(AFrame), PopToTop(Index),
and a few Properties: StackTop; Frames[Index: Integer]; Count;
Should be self explanatory.
The Frame at StackTop is the visible one. When doing ops like Back/Previous you don't need to know what frame was before the current one :)
When creating the Frame you can create and push it in one go FrameStack.Push(TAFrame.Create) etc, which creates it calls the BeforeShow proc and makes it visible, returning its index in the stack :)
But it does rely heavily on Inheriting your frames from a common ancestor. These frames all (in my Case) have procedures: BeforeShow; BeforeFree; BeforeHide; BeforeVisible.
These are called by the FrameStack Object during push, pop and top;
From your main form you just need to access FrameStack.Stacktop.whatever. I made my Stack a global :) so it's really easy to access from additional dialogs/windows etc.
Also don't forget to create a Free method override to free all the frames ( if the owner is nil) in the stack when the app is shut down - another advantage you don't need to track them explicitly:)
It took only a small amount of work to create the TFrameStack List object. And in my app work like a dream.
Timbo
I also use the approach described by #Tim Sullivan with some addition. In every frame I define the procedure for the frame initialization - setting default properties of its components.
TAnyFrame = class(TFrame)
public
function initFrame() : boolean; // returns FALSE if somesthing goes wrong
...
end;
And after the frame was created I call this procedure.
aFrame := TAnyFrame.Create(nil);
if not aFrame.initFrame() then
FreeAndNil(aFrame)
else
... // Associate frame with form and do somthing usefull
Also when you change visible frame it is not necessary to destroy previous one because it can contains useful data. Imagine that you input some data in a first frame/page, then go to the next, and then decide to change data on the first page again. If you destroy previous frame you lost the data it contains and need to restore them. The solution is to keep all created frames and creates new frame only when it is necessary.
page_A : TFrameA;
page_B : TFrameB;
page_C : TFrameC;
current_page : TFrame;
// User click button and select a frame/page A
if not assigned(page_A) then begin
// create and initialize frame
page_A := TFrameA.Create(nil);
if not page_A.initFrame() then begin
FreeAndNil(page_A);
// show error message
...
exit;
end;
// associate frame with form
...
end;
// hide previous frame
if assigned(current_page) then
current_page.hide();
// show new page on the form
current_page := page_A;
current_page.Show();

Resources