Graphical List / Draw List lazarus / 39-Graphics ---------------------------------------------------------------------- kupferstecher · 2024-10-21 16:49 Hi all, I'd need some advice on how to implement a graphical list. The (linear) list consists of different elements, that should be user drawn according to the contents. My first attempt was to use a scrollbox and just dynamically place panels with subcontrols. That worked well for small lists, but with more and more items, things got slow. I think the bottleneck is the scrollbox that triggeres a redraw of all elements once a control was removed or added. I'm now abandoning that approach and only want to draw what is actually visible on the screen. TDrawGrid seemed suitable for that and a first test was promising, but scrolling is quite odd, it seems that the scrolling is connected to the selected cell, while I just want to scroll the view and no selection. Also dragging the scrollbar doesn't immediately update the view. As the functionality that I need is not too much I think about programming such a component from scratch (If there is not a recommendation for a suitable control). And I wonder what would be the best way to do. Requirements: - The control should provide a handler/callback for each item like the DrawGrid providing a Canvas, on which can be drawn. - Random dimensions for each element, but always one item below another - Horizontal scrolling if it doesn't fit on the window - Flicker-free - Smooth pixel-wise scrolling (lower priority) - No internal data, just a pointer for user data in each item could be enough - Mouse control, handlers for clicks, double clicks My basic idea is to use a TCustomControl containing two scrollbars and copying the user drawn canvas to the correct positions according to the scrollbars. Would it be better to use a Canvas for each visible item, so when a new item becomes visible only the new item has to be redrawn and the existing ones could just be copied from the buffers, or should every element just be redrawn to avoid cache issues? Also what kind of canvas would be advisable regarding speed, a TBitmap, a TBGRABitmap? The first one could mean (slow) API-calls of the operating system, but could have faster copy mechanisms (Just guesses). Another possibility would be to dynamically place a seperate control for each visible item and move them around according to the scrollbars. The hope would be that moving is efficiently handled by the OS on the GPU and wouldn't involve manual redrawing or data copying using the CPU. But I'm not sure there either. It also could mean a lot of API-calls and even redraws. Or are there other approaches? I'm happy for any comments. Thanks! ---------------------------------------------------------------------- jamie · 2024-10-22 22:21 Sounds like you are looking for vector formats. Those are element by element drawn and can be cached in a storage container. WMF, SVG etc ---------------------------------------------------------------------- kupferstecher · 2024-10-23 15:11 It's a bit difficult to describe. I actually want to display data, so when using vector graphics I would have to create the vector graphics in realtime. I'm not sure if this is a good solution. Actually the TDrawGrid (with just using one column) would be fine if only the scolling behaviour would be different. So the question could also be, how to reimplement a (simplified) draw grid in a way that it allows pixel wise scrolling with "live update" while scrolling. ---------------------------------------------------------------------- Handoko · 2024-10-23 16:17 Lazarus TCanvas is good for cross platform compatibility, not optimized for performance and it is slow. It is okay for beginners to learn graphics programming, other than that it almost has zero real world usage for graphics programming. TBGABitmap is as slow as TBitmap. In some cases it is about 3% slower than TBitmap, I tried and benchmarked it so I can sure to say about it. Unless you're using the 'special' features of TBGRABitmap that internally using OpenGL. TBGRABitmap's and TBitmap's performances are no better than TCanvas, don't use any of them. Alternatively, you can consider Graphics32. I haven't tried it but it sounds promising: https://github.com/graphics32 If you're willing to learn, try OpenGL. I wrote a test that animates thousands of vector shapes using TOpenGLControl component, run perfectly smooth at 60 FPS even on 15 years old laptops. For TCanvas/TBitmap/TBGRABitmap, you usually will start to notice some sluggishness if the object count reaches about 100 even you heavily optimized your code. I still have no idea what you want to achieve. Can you provide some screenshots or sketches? Here we have many skilled programmers, maybe they can provide you the solution but you need to provide us more information. Or if you don't need it in a hurry, you can contact me personally. The basic features of my graphics engine has already work as I want it to, it internally uses TOpenGLControl so it should be cross platform too. I don't mind to subtract the code and make some necessary changes to suit your case. It is free but only if you can wait because I don't do programming whole day long. ---------------------------------------------------------------------- kupferstecher · 2024-10-23 17:29 Thanks for the reply! The TBitmap/TBGRABitmap comparison is very interesting. Attached an example, what I have in mind. The "table" I'm talking about is in the left of the screenshot (read frame). There it is a list of interfaces and clicking an item opens a detailed view with settings (middle and right box). In this application each item of the list is a panel and that I create on runtime and populate it with several controls, like TLabel, TImage and I draw the background manually in the Paint procedure. The list is placed on a scrollbox. As this list with ports/interfaces won't get too long, this appoach works fine. But for an other usecase with a similar list I need several hundred of entries and then the scrollbox approach is getting too slow. So as said, I tried to use the TDrawGrid component instead and I just draw the contents in the CellDraw event. In general it works, but scrolling is not nice. Opengl I would like to avoid, I think performance wise it should be possible to render such a list with LCL means. There is no animatition or anything and the contents are not too complicated, only the smooth scrolling is the critical point. Hope my intention is a bit clearer now. ---------------------------------------------------------------------- TRon · 2024-10-23 17:41 @kupferstecher: looks to me like something for a listview in virtual mode using custom drawing. Perhaps even a normal listbox using custom drawing (but a listbox doesn't have a virtual mode). ---------------------------------------------------------------------- kupferstecher · 2024-10-23 18:02 TListView I saw before, but in the wiki it said that the examples perhaps only work under Windows, so I though it might not be too reliable. But I'll have a further look. What does "virtual mode" mean? ---------------------------------------------------------------------- Handoko · 2024-10-23 18:16 Quote from: kupferstecher on October 23, 2024, 07:29:18 pmThe TBitmap/TBGRABitmap comparison is very interesting. In 2016 (if I remember correctly), here we had a graphics contest. I was preparing my 2D vertical scrolling spaceship shooting game. I wanted to use purely Lazarus components, without any third party component. Finally the basic prototype seemed to work correctly, the graphics was drawn using TCanvas lines only, no fill color, no bitmap images. The performance wasn't good, although all the objects were just some simple line drawings. So it was the time for optimizing the code. Not much can be done. If the object count reached about 150, the game wasn't playable. I tested it on an Intel G2020 machine, which was a bit faster than Core2 Duo. I heard people frequently talk about BGRABitmap. Some searching on the web, I learned we can easily switch our TCanvas/TBitmap code to use TBGRABitmap easily. That really was a good selling point. So I tried it. To my surprise, I didn't notice any performance differences. I modified the code for benchmarking. The result shown TBGRABitmap was some percentages slower than TCanvas. I haven't inspect into TBGRABitmap's source code, but that makes me think TBGRABitmap was actually TCanvas/TBitmap + some extra codes. I could be wrong but that is what I believe. If TBGRABitmap wasn't that slow I wouldn't start learning OpenGL. ---------------------------------------------------------------------- TRon · 2024-10-23 18:20 Quote from: kupferstecher on October 23, 2024, 08:02:02 pmTListView I saw before, but in the wiki it said that the examples perhaps only work under Windows, so I though it might not be too reliable. tbh I did not considered that. That what you would like to accomplish is basic functionality of the component so I can't really imagine this ot to be working on/for other widgetsets. If something does fail then there is also a custom drawn version that could perhaps be used (TCDListview) QuoteWhat does "virtual mode" mean? In basics it means that the data that you would normally store inside the items of the Listview itself are not really stored there but are requested by the component when they are needed. That way you can store data in any custom format that you desire and retrieve the actual contents when the component asks for it (in your particular case several pieces of information). Virtual mode allows you f.e. to set the number of items in the listview to a certain number (which can virtually be unlimited) without occupying the memory that it usually would take (theoretically allowing for a unlimited amount of items 'stored' in the list). I do not know for sure if it works the same for every widgetset but that also means that only the visible items  (or portions thereof) are drawn (if you put the list in customdrawn mode then the component will request to draw only those parts). ---------------------------------------------------------------------- Handoko · 2024-10-23 18:30 Quote from: kupferstecher on October 23, 2024, 08:02:02 pmTListView I saw before, but in the wiki it said that the examples perhaps only work under Windows ... Not sure it is updated or not. See the screenshot below. Source: https://wiki.lazarus.freepascal.org/Roadmap ---------------------------------------------------------------------- kupferstecher · 2024-10-23 19:59 OK, sounds promissing. I'll try it! Thank you! ---------------------------------------------------------------------- kupferstecher · 2024-10-23 22:03 Now I tested the ListView and ListBox, both owner drawn, see attached screenshots. From vertical scrolling they are both good, fast and with realtime update whle scrolling. Scrolling is always alligned to a line, no pixel scrolling, but that actually doesn't matter. The ListView has no property for the row height and after a quick internet search I still don't know how to set the height of the rows. Seems some hack is required, like assigning an (empty) ImageList that has a height property. The Listbox on the other hand has a height property, but doesn't support horizontal scrolling, seems the width of each line is according to the listbox size. Also the number of rows is determined by the items property (TStrings). So no virtual mode as @TRon already pointed out. But I could live with that, I think. So still not the perfect solution, yet. Or did I miss some properties/settings? Btw. the draw callback for the TListBox is fired for all visible items once there is a change, while the TListView seems to buffer each item seperately, as only the "new" items that enter the screen while scrolling are called for drawing. ---------------------------------------------------------------------- zeljko · 2024-10-24 11:11 Look for TVirtualStringTree, IMO that's something you're searching for. ---------------------------------------------------------------------- dbannon · 2024-10-24 11:33 Quote from: kupferstecher on October 24, 2024, 12:03:45 amNow I tested the ListView and ListBox, both owner drawn, ... Don't confuse Owner Drawn with Virtual Mode. I use ListView in Virtual Mode and it works well on Windows, Mac and Qtx but gtk2 still draws the entire contents each time. The idea is you have the content stored in some fast structure, I use FPList, you tell ListView to only render the content that is visible. The list view says, "hmm, lines 356-396 are visible, I'll ask the data structure what I should show for those lines". So, if you have a large set of data, maybe > 20K lines OR, heavy rendering of an image per line, its far more efficient. I also have several Indexes into that data structure so sorting the display, in my case, alphabetically or by date is easy. Its well doc'ed on the wiki (from memory). Davo ---------------------------------------------------------------------- kupferstecher · 2024-10-24 21:26 Quote from: zeljko on October 24, 2024, 01:11:38 pmLook for TVirtualStringTree, IMO that's something you're searching for. Thanks, I'll also have a look at it! Quote from: dbannon on October 24, 2024, 01:33:30 pmQuote from: kupferstecher on October 24, 2024, 12:03:45 amNow I tested the ListView and ListBox, both owner drawn, ... Don't confuse Owner Drawn with Virtual Mode. Can you eleborate a bit more? What I meant with owner drawn is that I use the handlers ListView1CustomDrawItem (ListBox1DrawItem, respectively) and draw on the canvas there. As I understood @TRon is that the virtual mode just means the component doesn't store any cell/list data. As said, drawing worked all right, the issue with ListView "only" was that I couldn't set the height of the rows. Neither individually nor one height for all rows. QuoteThe idea is you have the content stored in some fast structure, I use FPList, you tell ListView to only render the content that is visible. The list view says, "hmm, lines 356-396 are visible, I'll ask the data structure what I should show for those lines". So, if you have a large set of data, maybe > 20K lines OR, heavy rendering of an image per line, its far more efficient.Yes, thats what I need ---------------------------------------------------------------------- TRon · 2024-10-25 05:22 Quote from: kupferstecher on October 24, 2024, 11:26:24 pmAs I understood @TRon is that the virtual mode just means the component doesn't store any cell/list data. True, but you still would need to retrieve the data somehow. That is what the ondatahint, ondata and onfinddata events are for when property OwnerData of the listview is set to true (also known as virtual mode). But.... depending on your situation you could probably also do without (just checked and as long as the index from the item matches with an index to your list of ports you should be good to go) QuoteAs said, drawing worked all right, the issue with ListView "only" was that I couldn't set the height of the rows. Neither individually nor one height for all rows. I am going to suggest something completely illogical here (it works for me tm ) I placed a listview on a form of a new project (e.g. no other events or properties set), added the formcreate event using the object inspector and added the following code: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i procedure TForm1.FormCreate(Sender: TObject);begin  // create connections list and add items to it (intentionally left blank)   // create imagelist  ImageList := TImageList.Create(Self);  ImageList.Width  := 1;  ImageList.Height := 64; // <- more or less the height in pixels of your item (it actually has a few more pixels to separate the items)   // prepare listview  ListView1.SmallImages := ImageList;  ListView1.Columns.Add;  ListView1.ViewStyle := vsList;   // use ownerdraw  ListView1.OnCustomDrawItem:= @ListView1CustomDrawItem;  // the illogical: _DON'T_ set OwnerDraw  //  ListView1.OwnerDraw  := true;   // fire up the engine (because not knowing how you store/obtain the data I took my own example)  ListView1.Items.Count := Connections.Count; // Connections is a TFPGObjectList where each item contains the necessary informationend;  Then I manually added the ListView1CustomDrawItem implementation. For me that looked something like: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i  procedure TForm1.ListView1CustomDrawItem(Sender: TCustomListView; Item: TListItem; State: TCustomDrawState; var DefaultDraw: Boolean);var  ARect : TRect;begin  DefaultDraw := false;  ARect := Item.DisplayRect(drBounds);   // actually draw the item (very dumb and naive)  case Connections[Item.Index].strings[0] of   'COM' :   begin   Sender.Canvas.TextOut(ARect.Left, ARect.Top + 12, 'COM');     Sender.Canvas.TextOut(ARect.Left + 48, ARect.Top, 'Serial port');     etc, etc.   end;   'TCP' :   begin     Sender.Canvas.TextOut(ARect.Left, ARect.Top + 12, 'TCP');     Sender.Canvas.TextOut(ARect.Left + 48, ARect.Top, 'IP/TCP Connection');     etc, etc.   end;   'UDP' :   begin     Sender.Canvas.TextOut(ARect.Left, ARect.Top + 12, 'UDP');     Sender.Canvas.TextOut(ARect.Left + 48, ARect.Top, 'IP/UDP Connection');     etc, etc.   end;  end;end;  edit: picture is of a more progressed doodle using a listview ---------------------------------------------------------------------- kupferstecher · 2024-10-25 23:03 Thanks for the example! So the key part is the ImageList with the height, I think. I'm not sure yet, if I want to go with such a hack. I hoped there is a better way to define the height. But if it works... Quote from: TRon on October 25, 2024, 07:22:30 amTrue, but you still would need to retrieve the data somehow. That is what the ondatahint, ondata and onfinddata events are for when property OwnerData of the listview is set to true (also known as virtual mode).OnData and so on I don't understand, yet. My way would have been to organise the data in a list on my own and use it in OnCustomDrawItem just like you did in the example. Perhaps OnData is needed for sorting or similar things? (In my current use case I won't need any sorting). Thanks again, the screenshot is nice! ---------------------------------------------------------------------- TRon · 2024-10-26 05:16 Quote from: kupferstecher on October 26, 2024, 01:03:36 amThanks for the example! You're welcome. Though, I did forgot do do something crucial in that example and that is to set the ownerdata to true (it was left out during copy- paste/cleanup). QuoteSo the key part is the ImageList with the height, I think. You are correct. The problem with classes that originate from (and now bound to) Windows is that all the Windows specific stuff can't be added by the LCL because the functionality might be missing for other backends. This is one of them because Windows allows for a itemheight message to be send to the control that allows to set the height of/for an item. Adding a item or subitem that actually uses an icon with a specific width and height would usually (e.g. without using customdraw) draw that icon in the list and thus has to reserve the height and weight to be able to draw that icon. The code as shown was the quickest solution I was able to find that doesn't actually requires for the icon to be drawn but still reserves height and width (to be able to draw the icon, which is actually not drawn at all). QuoteI'm not sure yet, if I want to go with such a hack. I hoped there is a better way to define the height. But if it works... If you do not like such a workaround (I can imagine) then forget using any component that originates from windows and go the custom-drawn route. Though having said that, I tried to make sense out of the TCDListView component but am completely clueless as to me that component seems unfinished ?). There might be better solutions out there as also suggested by others. Personally I never managed to have found a list that is able to contain (a virtual amount of) custom data (it always seems to boil down to some sort of tree) except for one and that was for databases (Delphi came shipped with a panel that could be used to placed other components on top and a list that contained as many of these panels that where visible and which replicated the panel with all it contained components (and the contents of these components could be mapped to underlying database fields)). I believe it was simply named TDBPanelList (or something to that extend). QuoteOnData and so on I don't understand, yet. My way would have been to organise the data in a list on my own and use it in OnCustomDrawItem just like you did in the example. Perhaps OnData is needed for sorting or similar things? (In my current use case I won't need any sorting). That is why I said you probably are able to get away without using them. It is a bit complicated to implement because you 'virtually' fill a listitem with all the data that you want to have displayed and doing so by retrieving the data form your list. Using the methods allows for the data to be obtained 'on the fly' and customized any which way you want. The example (indeed) simply maps the index from the listitem to the index of the list that contains all the data, and in our example that works perfectly. There are many circumstances in which this is not possible and you actually have to obtain the data 'on the fly'. QuoteThanks again, the screenshot is nice! You're most welcome and even though I am not sure if this trick works for every widgetset (please do try and test) feell free to ask more in case you have questions. fwiw: I am still looking for an example that properly shows how to use the ondataXXX events (as there doesn't even seem to be an example anywhere in the source-tree nor the wiki) but I'm afraid the best I would be able to come up with is the documentation from (old) Delphi that explained these kind of things into details. ---------------------------------------------------------------------- wp · 2024-10-26 08:56 Quote from: TRon on October 26, 2024, 07:16:20 amI am still looking for an example that properly shows how to use the ondataXXX events Here is an example showing a Listview in virtual mode using the OnData event