Listbox CellBackgroundPaint not updating properly

Hello. I have three listboxes pulling data from a database. Listbox A holds the data, Listbox B holds the column header information and is located directly above Listbox A, and Listbox C holds the row header information and is located directly to the left of Listbox A. Listbox B and C have background paint to color the entire listbox. Listbox A has Mod 2 row shading. All three have return True on background paint to remove row selection highlighting. Listbox A can contain a lot of information, so both vertical and horizontal scrollbars exist.

I have two variables, selectedRow and selectedColumn. When a cell is selected in Listbox A, it updates these variables to the cell selected. If a row header is chosen in Listbox C, it updates selectedRow; and is a column header is chosen in Listbox B is updates selectedColumn.

For Listbox A during the background paint, it reads the selectedRow and selectedColumn variables and places a box/border around that cell. Every cell with a matching column gets a left and right border; every cell with a matching row gets a top and bottom border. For Listbox B, the matching column gets a left and right border; for Listbox C, the matching row gets a top and bottom border. It creates a bordered path to show which item a user chooses in a large matrix filled in Listbox A.


In the image above, the border is created correctly, but to the lower right you’ll see some artifacts left from a previous selection. This is one issue, and the other is sometimes during a selection, one of the borders won’t be entirely drawn. This happens intermittently, not with every new selection. It appears to be not clearing the background paint on every instance. If I scroll so the issue area is outside of the viewable listbox and return, it is corrected.

So is there something I’m doing incorrectly? Or is there a way to clear the listbox background paint (which I thought happened automatically) without clearing the listbox as a whole?

Listbox A CellBackgroundPaint code:

[code]If row Mod 2 = 1 Then
If row = selectedRow Then
If column = selectedColumn AND selectedColumn < me.ColumnCount Then
g.ForeColor = RGB(240,249,249) // light blue
g.FillRect(0, 0, g.Width, g.Height)
g.ForeColor = RGB(0,0,0) // black
g.DrawLine 0,0,g.Width,0
g.DrawLine 0,g.Height-1,g.Width,g.Height-1
g.DrawLine 0,0,0,g.Height-1
g.DrawLine g.Width-1, 0, g.Width-1, g.Height-1
ElseIf column < selectedColumn AND selectedColumn < me.ColumnCount Then
g.ForeColor = RGB(240,249,249) // light blue
g.FillRect(0, 0, g.Width, g.Height)
g.ForeColor = RGB(0,0,0) // black
g.DrawLine 0,0,g.Width,0
g.DrawLine 0,g.Height-1,g.Width,g.Height-1
Else
g.ForeColor = RGB(240,249,249) // light blue
g.FillRect(0, 0, g.Width, g.Height)
End
Else
If column = selectedColumn AND selectedColumn < me.ColumnCount AND row < selectedRow AND selectedRow < me.LastIndex +1 Then
g.ForeColor = RGB(240,249,249) // light blue
g.FillRect(0, 0, g.Width, g.Height)
g.ForeColor = RGB(0,0,0) // black
g.DrawLine 0,0,0,g.Height-1
g.DrawLine g.Width-1, 0, g.Width-1, g.Height-1
Else
g.ForeColor = RGB(240,249,249) // light blue
g.FillRect(0, 0, g.Width, g.Height)
End
End
ElseIf row Mod 2 = 0 Then
If row = selectedRow Then
If column = selectedColumn AND selectedColumn < me.ColumnCount Then
g.ForeColor = RGB(255,255,255) // white
g.FillRect(0,0, g.Width, g.Height)
g.ForeColor = RGB(0,0,0) // black
g.DrawLine 0,0,g.Width,0
g.DrawLine 0,g.Height-1,g.Width,g.Height-1
g.DrawLine 0,0,0,g.Height-1
g.DrawLine g.Width-1, 0, g.Width-1, g.Height-1
ElseIf column < selectedColumn AND selectedColumn < me.ColumnCount Then
g.ForeColor = RGB(255,255,255) // white
g.FillRect(0,0, g.Width, g.Height)
g.ForeColor = RGB(0,0,0) // black
g.DrawLine 0,0,g.Width,0
g.DrawLine 0,g.Height-1,g.Width,g.Height-1
Else
g.ForeColor = RGB(255,255,255) // white
g.FillRect(0,0, g.Width, g.Height)
End
Else
If column = selectedColumn AND selectedColumn < me.ColumnCount AND row < selectedRow AND selectedRow < me.LastIndex +1 Then
g.ForeColor = RGB(255,255,255) // white
g.FillRect(0,0, g.Width, g.Height)
g.ForeColor = RGB(0,0,0) // black
g.DrawLine 0,0,0,g.Height-1
g.DrawLine g.Width-1, 0, g.Width-1, g.Height-1
Else
g.ForeColor = RGB(255,255,255) // white
g.FillRect(0,0, g.Width, g.Height)
End
End
End

If me.ScrollPosition <> lbRowHeader.ScrollPosition Then
lbRowHeader.ScrollPosition = me.ScrollPosition
End

If me.ScrollPositionX <> lbColumnHeader.ScrollPositionX Then
lbColumnHeader.ScrollPositionX = me.ScrollPositionX
End

Return True[/code]

Thanks,

  • Jason

Why ?

A ListBox have a Heading Property to set the Column names.

And I do not understand what Listbox C is meant to do. But this may be just me.

There is already a way to get the Selected Row: ListBox.Selected .

At last, a skeleton project will be better to understand (or try to) what happens.

I’m not sure why you use Listbox B for column headers, instead of just using the column heading options for Listbox A. For Listbox C, you seem to be wanting something similar to a column or pane “freeze” in a spreadsheet, so that some lefthand data can remain visible even on horizontal scrolls. The standard listbox does not support that, which is probably why you did this method. But FYI, there is a third-part listbox replacement I like which does allow you to “lock” column(s) on the left. So you can fill the first column(s) with what you put in Listbox C now, mark them locked, and let it take care of it for you. It also does plenty of other things like row or column spanning, variable height rows with word wrap and editing, support for container controls in a cell (meaning you can make a cell do about anything), and some other pretty powerful stuff. See DataView

Without examining your code but just to answer your question, one possibility may be to store your last selected row and column in a module level property (or subclass the list and store them), then when doing your new selection, invalidate the previous row and column to force them to be refreshed:

me.InvalidateCell( mLastRow, -1)
me.InvalidateCell( mLastCol, -1 )

Although you are requesting the entire row or column to be invalidated, it is smart enough to only paint the visible cells. At least according to the docs.

I’m using listboxes for the column and row headers for coloring purposes and have frozen data. Currently, you cannot color a header in a listbox, nor can you lock a column in place with a horizontal scroll. The purpose of having the selected cell information as variables is so all three listboxes can modify that information. IE: if someone selects the column header cell in Listbox B and the row header cell in Listbox C, it will still highlight/frame the crossing cell. It allows users to easily see the headers pertaining to their selection, or select headers to find the value that is at the cross section.

I’ve tried using Invalidate, but it has not solved the issue.

Oh yeah, forgot you can’t to that with a standard listbox. I use the HeaderBackgroundPaint and HeaderTextPaint events of DataView. Guess I am spoiled.

Perhaps the listbox cells are getting paint events but in some instances is not computing the graphics bounds consistently and it ends up missing an edge here or there. Have you tried painting a different background color on those cells? Visually that may direct the user’s eyes even faster than the borders. And it may (or may not) work around the other problem.

InvalidateCell() should work just fine.

I’ve thought about this. For aesthetic reasons, I’m trying to use the borders first. But I may try coloring it temporarily just to see the results.

I agree. And I can’t explain why it doesn’t. When scrolling the primary listbox, without changing any cell text or creating a new cell selection, the borders get redrawn correctly after going “out of view” and back into view. It’s only when the listbox is static, and a new cell selection is made that the border drawing is incorrect (yet the background color is correct).

[quote=336173:@Jason Fink]I’ve thought about this. For aesthetic reasons, I’m trying to use the borders first. But I may try coloring it temporarily just to see the results.

I agree. And I can’t explain why it doesn’t. When scrolling the primary listbox, without changing any cell text or creating a new cell selection, the borders get redrawn correctly after going “out of view” and back into view. It’s only when the listbox is static, and a new cell selection is made that the border drawing is incorrect (yet the background color is correct).[/quote]

Well, why don’t you try adding this at the top of your CellBackgroundPaint() event, before your IF block:

    g.ForeColor = RGB(255,255,255) // white
    g.FillRect(0,0, g.Width, g.Height)

It would be a quick test to see if that worked around the issue.

Jason, is this Windows or OS X? If it is Windows, it has some odd peculiarities related to the what gets redrawn when cells are dirty and an InvalidateCell( ) is called for.

[quote=336174:@Douglas Handy]Well, why don’t you try adding this at the top of your CellBackgroundPaint() event, before your IF block:

    g.ForeColor = RGB(255,255,255) // white
    g.FillRect(0,0, g.Width, g.Height)

It would be a quick test to see if that worked around the issue.[/quote]
Same Issue. I also tried using just background color without borders:

Merv, it’s on Windows 10. I’ve tried Invalidate, InvalidateCell, and Refresh in the CellClick, nothing has worked yet.

I’ve resorted to a workaround:

In the CellClick code, before the selected row and column are assigned to variables, I disable then reenable Listbox A. Every click draws correctly without leaving old artifacts behind.

Does anyone know why this happens though? @Norman Palardy maybe?

  • Jason

In that same place in the code (or another place if necessary), try: InvalidateCell(-1, -1) to refresh every cell. If that removes the artifacts, let me know and I’ll say what I think is going on.

It appears to be working correctly, but I did try this previously when it was suggested and the issues still appeared. Interesting.

Is it possible you just tried this, that was listed above:

me.InvalidateCell( mLastRow, -1 ) me.InvalidateCell( mLastCol, -1 )
There is a difference. Let me know and I’ll explain what is likely going on.

Yes, I had suggested trying to invalidate just the row and column with the artifacts, in response to the question of a way to avoid having to redraw the entire listbox. InvalidateCell(-1,-1) effectively causes the entire visible area of the listbox to be redrawn.

That said, it should still be more efficient than enable/disable which likely does several other things in addition to evidently invalidating the entire visible region.

THE REST OF THE STORY: In the Summer of 2014, I ran into a bug on the Mac in Cocoa, where a single InvalidateCell(MyRow, MyCol) was causing EVERY visible cell to be repainted. Xojo fixed that bug which greatly helped the performance on Cocoa. But in the midst of all this, it was discovered that on Windows, a single InvalidateCell(MyRow, MyCol) repainted a subsection of the visible ListBox. And depending on, what cell was clicked, up to half the cells are repainted (and the remaining cells were not). It always picks a subsection rectangle that looks to be chosen to minimize the number of cells to be repainted.

Xojo did fix that bug on a second pass in a beta cycle, but it started causing other problems in the ListBox drawing, so after a few patches, they pulled out the patched code on the Windows side and left things as they were. Performance was not an issue on Windows, like it was on the Mac, so it was decided to just leave it alone.

There are still some underlying issues that popup from time to time. For example, on one of my grids a column has a double thick border drawn along the right side of the column, all the way down. If I edit the cell, when the ActiveCell gives up the focus, the entire column changes the thick border to a thin border, it loses 1 pixel in the border for some reason. I have to use an InvalidateCell(-1, -1) in this case. Little things like this, that over time you get worked out of the interface. Usually an InvalidateCell(-1, -1) in problem areas does the trick.

I did try this first:

me.InvalidateCell( mLastRow, -1 ) me.InvalidateCell( mLastCol, -1 )
When it didn’t work, I tried -1, -1; and at the time, the problem persisted. Since it is working now, I’ll continue using it. Thanks.

  • Jason