DesktopListbox - questions about the PaintCellText event handler

The docs for DesktopListbox include this, for the PaintCellText handler:

Returning True means the user has handled the text paint and no other processing is to be done with the text. In this case, the user is responsible for text highlighting. Text highlighting is currently only done for the hierarchical listbox style. Returning False means the user wants the default text drawing. In this case, the text will be highlighted as appropriate (according to platform) for you automatically.

This implies that if I want to do my own thing in regard to row and text highlighting, then I’d better Return True in the event handler. Well, I do want to do my own thing. I have two separate row highlight colours, and I don’t want to text colour changed to white if a row is highlighted.

Here is a simplified version of my PaintCellText handler, showing what gets put into the various columns:

PaintCellText (g as Graphics, row as integer, column as integer, x as integer, y as integer) As Boolean

Var  reg As RowSet, offset As Double, statPict As Picture

if  (column=0)  then

  select case me.CellTagAt(row, 0)
  case app.STAT_UNREAD
    statPict = msunread

    // More cases to set statPict

  end select

  // Code to set offset

  g.DrawPicture (statPict, 5, offset)
  Return False

end if

if  (column=1)  then
  // Code to set statPict
  g.DrawPicture (statPict, 5, 2)
end if

// Code here to obtain some database info based on an id in the rowtag.
// Use part of the database info to set g.DrawingColor, and ...

g.FontName = "Arial"
me.CellTextAt(row,2) = reg.Column("col2data").StringValue
me.CellTextAt(row,3) = reg.Column("col3data").StringValue
me.CellTextAt(row,4) = reg.Column("col4data").StringValue
me.CellTextAt(row,6) = reg.Column("col6data").StringValue

Return False

End Sub

This code works as desired, with my various row highlighting colours and text colours as I decide, with no automatic row or text highlighting. But I’m returning False and not True, so is that a documentation error? If I return True instead, then the text colour goes white when the row is highlighted and the font is wrong for the whole row (System instead of Arial).

The other issue is that the code for setting CellTextAt for columns 2-6 is executed several times per row (when column=2,3,4,5,6) to no purpose. I want to avoid that as when column=1, all the relevant cells are filled. But if I put something like:

if  (column>1)  then Return False

at the head of the event handler, I’m back to having the event handler messing with font and text colour again.

I feel I’m mising something fundamental about the Listbox.

No error there, with True, then this example only writhe Test once:
image

with false:
image

I’m sorry I can’t help you with only looking at your code.
Can you create a sample project that I can run here?
Are you working on Mac or Windows? Light mode or Dark mode?

It’s always been a little known side effect that you can manipulate the graphics properties in the event while letting the framework do the actual drawing. Return true / false tells the framework whether or not to draw the text.

The advantage to this is that you can let the framework do the positioning while you adjust only the properties you desire such as color. It would be project breaking were they to change this behavior now.

You need to only do drawing in the Paint events. DO NOT ADJUST CELL CONTENTS. That would logically create an infinite loop, but there are framework level stops to prevent this from totally failing. You need to find somewhere more appropriate to adjust cell contents. Paint is NOT the correct event to do so.

Your code needs to be prepared to allow the system to call Paint as frequently as desired. The system will call Paint when it determines it’s time to redraw, which could be far more than you expect.

I can’t explain what you describe by returning true being the wrong color, the code you’ve shared does not adjust color. My wild guess would be that your code doesn’t execute the way you’re expecting.

2 Likes

Mac, Light mode, 2023r4.

I’ll work on a small example.

By “only drawing”, are you saying that one should not be setting the text content of cells in the paint event? In the method that populates the listbox, I’m deliberately only setting the celltag of columns 0 and 1. I do nothing with the other columns at that point. But it’s up to the user, not me, as to how many rows there are in the listbox. Could be a few hundred, or as many as 20 to 30 thousand (I have one with 20k entries for testing). With 20k rows, it takes several seconds to fill the listbox if I do all the work in the population method (long enough for the spinning pizza to appear). By setting only those two celltags, and some other info in the rowtag, there’s enough info for PaintCellText to populate the rows that the framework deems need repainting. And that number of rows is considerably fewer than the number of rows in the listbox. I’d love to do wll the row/column populating upfront. But it takes too long and if I change that now it will annoy the users. And it would annoy me, too.

If I don’t do that in PaintCellText, then where do I do it?

I guess I didn’t make it clear (sorry) that the comments in the posted code replace/summarise code that is there in my actual app. The actual code is adjusting the text colour (g.DrawingColor). The point is that the docs say that if I have done all that I want done, and don’t want to have the default action taken, I should return True. If I do that, then I get the wrong font, and the wrong text colour (white) when the row is highlighted. So I return false, and the Framework does not then interfere with what I’ve coded. But it seems to me that is the reverse of what the docs say.

I can’t stop the framework deciding when to call the event - no problem with that. But I’d like to avoid unecessary overhead by having the event handler be essentially a no-op when column = 2,3,4,5,6. I’ve already written the text in those columns, why would I want to do it several more times? But if I do that, I get white text in all rows and columns, whether or not the row is highlighted. And the text is actually there - I’ve checked in the debugger.

Yes, but I also mean expensive functions such as image resizing or extreme math. Don’t touch any controls, that would logically cause redraws. Don’t change column widths, that would logically cause redraws. The point is that by the time Paint occurs, you should know what your drawing should look like and draw it. That’s it.

Paint events occur when the system decides it needs to paint again, and it wouldn’t be unreasonable for it to occur 60 or more times per second (not that it does all the time, but this is how smooth animation is achieved).

There are tricks to work around this such as hiding the Listbox during population. Since UI updates for the things we implement don’t occur until the end of the event stack the end effect is invisible to the user.

20k rows is also a lot to have in the control at one time. With that much data you could improve performance with lazy loading and data-on-demand. Not simple things to do. I think someone has a data-on-demand listbox, but I have personally never used it.

You’re not populating cell tags though, you’re populating the actual cells. Think about it:

System: I’d like to paint this cell, what should I do?
You: Change the color, and then change the value.
System: Okay, if I change the value, I need to repaint it again, what should I do?
You: Change the color, and then change the value.
System: Okay, if I change the value, I need to repaint it again, what should I do?
You: Change the color, and then change the value.

Personally, I think this should raise a StackOverflowException, but it was quietly muffled to silently do nothing at all before the big API 2.0 move toward exceptions.

I can’t help with your actual problem unless I see your actual code. I find that many times the root cause of the problem gets snipped out when adjusting code for the forum. I’m trying to be as helpful as I can in a generic abstract way.

Returning true should get you absolutely nothing at all with the code above, you never draw any text. Return true = “I drew the text, Xojo please don’t” and return false = “I didn’t draw the text, Xojo please use the current state of the Graphics object to draw the text”. That is all.

I’m sorry the editor for the documentation isn’t as familiar with the framework, but Xojo doesn’t pay me for everything I’ve learned over the last 10 years. This should be a reflection on Xojo’s care and attention to detail. I am sharing what I know with you now.

I can’t comment on this without seeing the actual project. If your code draws the text, you need to draw it every time Paint occurs and return true. If you let the framework do it, return false.

I’ve built you a super simple sample showing how the return value affects drawing: Simple DesktopListbox.xojo_binary_project

I’m not saying that you need to share your entire project here on the forum, I’m just saying that I can’t help solve your problem without being able to evaluate the complete picture.

2 Likes

Thanks for that. Carefully reading your reply has made me realise that my misunderstanding of the listbox is that I assumed that writing text into a cell caused that text to be visible (just as writing text to a DesktopLabel causes that text to be visible). Instead, it seems that the cell text set by CellTextAt is actually just another form of celltag, and that for any text to be visible it has to be painted, either explicitly by me via g.DrawText of some text, or by letting the system do it in which case it paints the CellTextAt text. This also perfectly accounts for my True/False confusion. Your tiny app was a help here, too.

This enlightenment allows me to make progress, but I have one further question: if the framework decides to refresh a row, is it guaranteed that the PaintCellText events fire in column order for each row one by one, that is starting with column=0, then column=1, and on through all columns excepting any that it skips? And only then proceeding to another row?

Thanks again.

There has never been any documentation statement to that effect, no. I don’t have an answer for you because the closest thing I’ve ever done is cache the Color.IsDarkMode value in cell 0, 0 (because it’s a slow function that needs to occur in a Paint event to be accurate).

It may happen that way, but can I ask why you’re hoping to leverage the order of cell drawing like this?

So that when the col-0 event fires I can read the database and set the CellTextAt for later columns, determine and save the text colour and style (italic or not) in a couple of properties for the listbox, and use them to set the drawing properties for later columns in this row, when those events fire. This means that for the later columns, the event handler does really very little. This is largely to reduce drawing overhead under Windows, which seems to struggle as I move splitter bars around in the app, particularly when this listbox is visible.

I wouldn’t trust that any column gets painted in any particular order. Instead, store the data for the row somewhere and check it whenever any column is painted, and fetch the data if necessary, caching it for when the rest of the row is painted.

As Eric said, I would recommend using an event other than Paint to read the database and load the data.

You might want to load the database into CellTags and then simply read the CellTag to know what to draw in the Paint event. The Paint event really should just be “I need to draw ASDF now”. You can fetch ASDF from the database and load it into CellTags in another event (such as Opening).

You could use your own class if you need to expand upon individual cells having certain properties. Store class instances that describe the text value and styling as CellTags, then fetch them from the tag in the Paint event.

I’m not sure about imSplitter, but with Einhugur’s WindowSplitter you can disable live resize on Windows (the switch is called Win Live Mode) and I suspect it was added for the same reason you’re describing.

3 Likes

DesktopListboxes can comfortably handle hundreds of thousands of rows, even a million (I’ve tried, exhaustively) without beachballing. The tests I’ve done suggest it does have an array buffering its data and it only draws the rows/columns visible onscreen.

If you’re seeing it behave slowing there are three likely causes:

a) the data source (database) from which you’re pulling data is slower than the listbox. On-demand loading of the listbox offers an improvement only where the data source is fast, e.g. an in-memory database; otherwise the benefit may be minimal if its fetched from spinning rust, row by row.

b) you’re doing something intensive row-by-row or cell-by-cell in a high-frequency event handler, notably PAINT. Do the logic outside the PAINT event-handler to the extent the text, any graphic and the format are defined before the PAINT occurs,

c) Your listbox has the vertical scrollbar visible while its being loaded. Don’t ask me why by it appears to force a PAINT each time the row is loaded, rather than waiting till all visible rows are loaded. Disable the scrollbar, load data and then enable it after is the fast way.