I have code that works just fine for my purposes except one niggling detail. Often when the COMCTL32.OCX ListView is freshly "populated" with items the auto-arrangement seems to fail.
I created a small program that demonstrates this, and the failures look like:
The failures are in positioning of the ListView items, as shown by the odd "gap" in the screenshot above marked with a red oval.
They don't occur every time (whack the Populate button a few times until you see a weird empty spot or a horizontal scroolbar appear). They can be "corrected" after the fact by either (a.) any adjustment of the size e.g. by resizing the Form by dragging an edge or corner, or (b.) doing what happens when you click on the Command2 button marked "Arrange" - see the code:
Code:
Private Sub Command2_Click()
Dim SaveArrange As ListArrangeConstants
With ListView1
SaveArrange = .Arrange
.Arrange = lvwNone
.Arrange = SaveArrange
End With
End Sub
Right now my big program does this flippity-flop of .Arrange once the entire ListView is populated each time. And no, just doing the flippity-flop after adding the 1st item to the ListView doesn't help.
The problem with this "fix" is that it can be pretty awkward. For one thing the ListView in the "real" application gets populated somewhat slowly, and for another during this slow population the user can double-click an item or cancel population before it is complete.
More info: it seems to be related to .LabelWrap = True and long text values for the item labels. It seems as if the problem disappears if I generate only short names in the test program or turn off LabelWrap. Neither of these is practical for my application though.
So my question is: Does anyone know if there is something that can be done to prevent this from happening at all?
I'm using the "old" OCX here because I need to use it to make use of the Common Controls 6.0 Win32 ListView, mostly for uxTheme (often referred to erroneously as "XP styles") purposes. I haven't tested with the new MSCOMCTL.OCX but that doesn't help me here.
I tried the following as a workaround in your Command1_Click event, and it seemed to work:
Code:
Private Sub Command1_Click()
Dim I As Long
With Me.ListView1
.Enabled = False ' Disable control to prevent flicker when changing LabelWrap property
.LabelWrap = False ' Temporarily disable label wrap
End With
'(Re)populate ListView1 with a new set of items with new images and text.
PopulateImageList1
With ListView1.ListItems
For I = 1 To 10
.Add , , MakeName(), Int(2 * Rnd()) + 1
Next
End With
With Me.ListView1
.LabelWrap = True ' Re-enabled label wrapping
.Enabled = True ' Re-enable control to redraw it without flicker
End With
End Sub
It could work for some situations but as I mentioned above I need to have the ListView be "live" as it gets populated since filling it up entirely can take a long time and the user must be allowed to double-click on an item before all items have been added.
That pretty much rules out flipping properties such as .Enabled and .Visible.
Sorry, I misunderstood/misread your post - I thought you just didn't want to keep calling your Arrange code as a workaround because it was flickering or would allow a user to double-click too soon.
I tried creating a timer based population to simulate your situation, and then tried a variety of promising looking window messages, but nothing has worked so far unfortunately.
I also noticed that resizing the window will cause it to recalculate the layout properly, but only after you're done adding items it seems. Maybe there is something wrong with the Add method of the control? Perhaps using the API to add items would circumvent this issue (I haven't had a chance to test this yet, but I'll try tomorrow if you don't get a chance)?
Well, I noticed that when you press the "Populate" button then the "Arrange" button the objects lined up without any gaps in-between. I then tried moving the arrange code into the Form_Load. This worked that majority of the but sometimes you would still receive the error such as in the first post.
Although, if the code is written
Code:
Private Sub Form_Load()
Dim SaveArrange As ListArrangeConstants
With ListView1
SaveArrange = .Arrange
.Arrange = lvwNone
.Arrange = SaveArrange
End With
Randomize
End Sub
It seems to arrange the items correctly on each click of the button as in comparison to the same code but preforming the "Randomize" first.
when you quote a post could you please do it via the "Reply With Quote" button or if it multiple post click the "''+" button then "Reply With Quote" button.
If this thread is finished with please mark it "Resolved" by selecting "Mark thread resolved" from the "Thread tools" drop-down menu. https://get.cryptobrowser.site/30/4111672
I tried the code in post #6 above, did not seem to make any difference, still seeing the gap intermittently when populating the listview.
You are correct! I wonder if dilettante might have over estimated the size of the array that the array actually includes the blank name? Does the same thing occur if the code for "MakeName()" is comment out and you change the for next?
Code:
For I = 1 To 10
.Add , , , Int(2 * Rnd()) + 1
Next
when you quote a post could you please do it via the "Reply With Quote" button or if it multiple post click the "''+" button then "Reply With Quote" button.
If this thread is finished with please mark it "Resolved" by selecting "Mark thread resolved" from the "Thread tools" drop-down menu. https://get.cryptobrowser.site/30/4111672
If a "blank item" was the problem then "Arrange" clicks and Form resizing wouldn't change anything. So unless I misunderstood your comment I don't think blank names are the problem.
I need long names in the test program or wrapping doesn't occur, and thus the problem doesn't exist. My "real" program is dealing with names that need to wrap.
Thanks everyone for taking a look though. More ideas are welcome!
Using SendMessage Me.ListView1.Hwnd, LVM_INSERTITEMW, 0, VarPtr(<LVITEM Structure>) seems to work to fix the problem in my initial testing anyway...I can provide the code if you want, I just have to clean it up a bit first.
Looks like my suspicions were correct, and the VB wrapper probably messed something up with its implementation.
Here's the sample code - you can just replace the code in your original demo with the following to test (it uses a timer to simulate chunked population so you can also see that dbl-clicking still works, so just add a Timer (Timer1) to your form):
Code:
Option Explicit
Private Const LVM_FIRST As Long = &H1000
Private Const LVM_INSERTITEMW As Long = (LVM_FIRST + 77)
Private Const LVIF_COLUMNS As Long = &H200
Private Const LVIF_DI_SETITEM As Long = &H1000
Private Const LVIF_GROUPID As Long = &H100
Private Const LVIF_IMAGE As Long = &H2
Private Const LVIF_INDENT As Long = &H10
Private Const LVIF_NORECOMPUTE As Long = &H800
Private Const LVIF_PARAM As Long = &H4
Private Const LVIF_STATE As Long = &H8
Private Const LVIF_TEXT As Long = &H1
Private Type LVITEM
mask As Long
iItem As Long
iSubItem As Long
state As Long
stateMask As Long
pszText As String
cchTextMax As Long
iImage As Long
lParam As Long
iIndent As Long
End Type
Private Declare Function SendMessage Lib "user32.dll" Alias "SendMessageA" (ByVal hwnd As Long, ByVal wMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
Private Function MakeName() As String
Const LETTERS As String = "abcdefghijklmnopqrstuvwxyz"
Dim I As Long
Dim IMax As Long
Dim J As Long
Dim Words() As String
IMax = Int(3 * Rnd()) + 1
ReDim Words(IMax)
For I = 0 To IMax
For J = 1 To Int(8 * Rnd()) + 2
Words(I) = Words(I) & Mid$(LETTERS, Int(26 * Rnd()) + 1, 1)
Next
Words(I) = StrConv(Words(I), vbProperCase)
Next
MakeName = Join$(Words, " ")
End Function
Private Sub PopulateImageList1()
With ListView1
.ListItems.Clear
Set .Icons = Nothing
With ImageList1.ListImages
.Clear
.Add , , LoadPicture(App.Path & "\DriveItem.gif")
.Add , , LoadPicture(App.Path & "\PhotoItem.gif")
End With
Set .Icons = ImageList1
End With
End Sub
Private Sub Command1_Click()
With Me.Timer1
If .Enabled Then
Me.Command1.Caption = "Populate"
.Enabled = False
Else
Me.Command1.Caption = "Stop"
.Interval = 50
.Enabled = True
End If
End With
End Sub
Private Sub Command2_Click()
Dim SaveArrange As ListArrangeConstants
With ListView1
SaveArrange = .Arrange
.Arrange = lvwNone
.Arrange = SaveArrange
End With
End Sub
Private Sub Form_Load()
Randomize
PopulateImageList1
End Sub
Private Sub Form_Resize()
If WindowState <> vbMinimized Then
With Picture1
.Move ScaleWidth - .Width, ScaleHeight - .Height
ListView1.Move 0, 0, ScaleWidth, .Top
End With
End If
End Sub
Private Sub ListView1_DblClick()
Debug.Print "DblClick " & Timer
End Sub
Private Sub Timer1_Timer()
Dim I As Long
Me.Timer1.Enabled = False
'(Re)populate ListView1 with a new set of items with new images and text.
With ListView1
For I = 1 To 10
Dim lt_ListItem As LVITEM
lt_ListItem.mask = LVIF_TEXT Or LVIF_IMAGE
lt_ListItem.pszText = MakeName
lt_ListItem.iImage = Int(2 * Rnd())
SendMessage .hwnd, LVM_INSERTITEMW, 0, VarPtr(lt_ListItem)
Next
End With
Me.Timer1.Enabled = True
End Sub
You are correct! I wonder if dilettante might have over estimated the size of the array that the array actually includes the blank name? Does the same thing occur if the code for "MakeName()" is comment out and you change the for next?
Nope, I modified the code a little assigned the makename() result to a variable and the rnd() result to a variable and then used those on the .Add statement. I also used debug.print to print the content of those vars as it went through the loop, all of them had a valid name and number. Nothing looked unusual except that it you keep clicking the populate button sometimes there will be a missing image.
Private Sub cmdPopulate_Click()
Dim I As Long
'(Re)populate ListView1 with a new set of items with new images and text.
PopulateImageList1
With ListView1.ListItems
For I = 1 To 10
.Add , , MakeName(), Int(2 * Rnd()) + 1
DoEvents
cmdArrange_Click
Next
End With
End Sub
Nope, I modified the code a little assigned the makename() result to a variable and the rnd() result to a variable and then used those on the .Add statement. I also used debug.print to print the content of those vars as it went through the loop, all of them had a valid name and number. Nothing looked unusual except that it you keep clicking the populate button sometimes there will be a missing image.
No, I meant remove makename() from the equation and see if the problem still occurs.
when you quote a post could you please do it via the "Reply With Quote" button or if it multiple post click the "''+" button then "Reply With Quote" button.
If this thread is finished with please mark it "Resolved" by selecting "Mark thread resolved" from the "Thread tools" drop-down menu. https://get.cryptobrowser.site/30/4111672
I don't think there's a problem with the MakeName function - I use it in my API based solution, and it works fine there.
My hunch is that the OCX wrapper is calculating the width of long text as a single line without taking wrapping into account, and therefore causing the item to be double-width. After finishing adding items, and resizing the control, the underlying DLL takes over the drawing, and "fixes" the problem. Using the SendMessage API goes directly to the DLL implementation and fixes the problem.
Using the SendMessage API goes directly to the DLL implementation and fixes the problem.
I'll give this a try now that I have time to get back to the issue again. It seems odd that the wrapper would cause this but anything is possible. Hopefully bypassing it to add items doesn't cause it to get out of sync with the actual ListView inside (for example throwing off the .ListItems collection's .Count and so on).
My hunch is that the OCX wrapper is calculating the width of long text as a single line without taking wrapping into account, and therefore causing the item to be double-width. After finishing adding items, and resizing the control, the underlying DLL takes over the drawing, and "fixes" the problem. Using the SendMessage API goes directly to the DLL implementation and fixes the problem.
Well, since the MakeName function controls the text maybe the output needs to be passed in to a function that determines if the text is long that the picture and if so put the overhanging text on a new line (wrap the text). As you say it could be a problem with the control itself and a splitter function may be the answer.
when you quote a post could you please do it via the "Reply With Quote" button or if it multiple post click the "''+" button then "Reply With Quote" button.
If this thread is finished with please mark it "Resolved" by selecting "Mark thread resolved" from the "Thread tools" drop-down menu. https://get.cryptobrowser.site/30/4111672
I have some vague recollection of a similar bug in a different control that could be bypassed by using the API, but it's pretty vague It's what gave me the idea to try the SendMessage API though.
I didn't test any further to see how the control properties might be affected though, so fingers crossed!
Using LVM_INSERTITEMW instead of LVM_INSERTITEMA with SendMessageA is a little bizarre. Normally it could cause a problem, but many of the Shell Common Controls like this one must be implemented oddly.
Unlike the Win32 controls they use another mechanism to "shift" between ANSI mode and Unicode mode, and so the A messages and W messages seem to work interchangeably for the current mode whether you use SendMessageA or SendMessageW.
I did try LVM_INSERTITEMA as well, just with a StrConv(MakeName(), vbFromUnicode) and it also seemed to work fine in limited testing. I forgot to update my example though.
Sure enough this seems to correct the problem with the initial arrangement of the populated items.
But as I suspected it breaks a lot of the wrapper's functionality too, such as the ListItems collection (.Count for example stays at 0). I don't see any way for the wrapper to resync with the underlying ListView either.
Darn, that's a shame. I assumed the OCX was just a wrapper for the various API calls and messages, and somehow MS just managed to muck up the one call (and everything else would work as normal). Obviously that was not a good assumption to make!
Sure enough this seems to correct the problem with the initial arrangement of the populated items.
But as I suspected it breaks a lot of the wrapper's functionality too, such as the ListItems collection (.Count for example stays at 0). I don't see any way for the wrapper to resync with the underlying ListView either.
Have you tried creating your own User Control to do the job?
when you quote a post could you please do it via the "Reply With Quote" button or if it multiple post click the "''+" button then "Reply With Quote" button.
If this thread is finished with please mark it "Resolved" by selecting "Mark thread resolved" from the "Thread tools" drop-down menu. https://get.cryptobrowser.site/30/4111672
I tried "tricking" the control by calling ListItems.Add , the SendMessage LVM_DELETEITEM on the new item, then SendMessage LVM_INSERTITEMA to add the item in hopes that the ListItems collection would maintain the count. Unfortunately it didn't work - the Count property still returns 0, so it seems at least partially synced, which is weird - I'm not sure why it would be updated after sending LVM_DELETEITEM and not after sending LVM_INSERTITEM.
Alright, I'm flailing away now, so I'll give it a rest after this, but using the LockWindowUpdate function along with the LVM_UPDATE message is showing some promise (again in very limited testing):
Code:
Option Explicit
Private Const LVM_FIRST As Long = &H1000
Private Const LVM_UPDATE As Long = (LVM_FIRST + 42)
Private Declare Function SendMessage Lib "user32.dll" Alias "SendMessageA" (ByVal hwnd As Long, ByVal wMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
Private Function MakeName() As String
Const LETTERS As String = "abcdefghijklmnopqrstuvwxyz"
Dim i As Long
Dim IMax As Long
Dim j As Long
Dim Words() As String
IMax = Int(3 * Rnd()) + 1
ReDim Words(IMax)
For i = 0 To IMax
For j = 1 To Int(16 * Rnd()) + 2
Words(i) = Words(i) & Mid$(LETTERS, Int(26 * Rnd()) + 1, 1)
Next
Words(i) = StrConv(Words(i), vbProperCase)
Next
MakeName = Join$(Words, " ")
End Function
Private Sub PopulateImageList1()
With ListView1
.ListItems.Clear
Set .Icons = Nothing
With ImageList1.ListImages
.Clear
.Add , , LoadPicture(App.Path & "\DriveItem.gif")
.Add , , LoadPicture(App.Path & "\PhotoItem.gif")
End With
Set .Icons = ImageList1
End With
End Sub
Private Sub Command1_Click()
With Me.Timer1
If .Enabled Then
Me.Command1.Caption = "Populate"
.Enabled = False
Else
Me.Command1.Caption = "Stop"
.Interval = 50
.Enabled = True
End If
End With
End Sub
Private Sub Command2_Click()
Dim SaveArrange As ListArrangeConstants
With ListView1
SaveArrange = .Arrange
.Arrange = lvwNone
.Arrange = SaveArrange
End With
End Sub
Private Sub Form_Load()
Randomize
PopulateImageList1
End Sub
Private Sub Form_Resize()
If WindowState <> vbMinimized Then
With Picture1
.Move ScaleWidth - .Width, ScaleHeight - .Height
ListView1.Move 0, 0, ScaleWidth, .Top
End With
End If
End Sub
Private Sub ListView1_DblClick()
Debug.Print "DblClick " & Timer
End Sub
Private Sub Timer1_Timer()
Dim i As Long
Dim j As Long
Me.Timer1.Enabled = False
'(Re)populate ListView1 with a new set of items with new images and text.
With ListView1
For i = 1 To 10
Win.LockWindowUpdate .hwnd ' Not supposed to be used except for Drag&Drop feedback according to MSDN, but I needed a quick way to stop flickering. Maybe you have a better idea?
.ListItems.Add , , MakeName(), Int(2 * Rnd()) + 1
For j = .ListItems.Count - 2 To .ListItems.Count - 1 ' Why these *2* items? I'm not sure, but it seems to be necessary.
SendMessage .hwnd, LVM_UPDATE, j, 0
Next j
Win.LockWindowUpdate 0
Next i
End With
Me.Timer1.Enabled = True
End Sub
Have you tried creating your own User Control to do the job?
You quickly get to the point where you might as well be writing C++ ATL or MFC programs. Life is too short, and clients too stingy with the hours they'll pay for.
Alright, I'm flailing away now, so I'll give it a rest after this, but using the LockWindowUpdate function along with the LVM_UPDATE message is showing some promise (again in very limited testing)...
Yeah, I see where you are going with that.
This might just be one of those things where I'll have to compromise. The client is already cranky about the delay in getting a new version of the application to test, so I'll just have to give them a choice between the Arrange code after populating or just living with the misarranged ListView item icons.
I don't know whether this stems from changes in the Common Controls that the VB6 controls were never updated to handle or from this application pushing the limits somewhere. It doesn't seem to be OS-dependent but I haven't tried testing on pre-XP systems yet. Doesn't really matter in the end - it is what it is.
This might just be one of those things where I'll have to compromise. The client is already cranky about the delay in getting a new version of the application to test, so I'll just have to give them a choice between the Arrange code after populating or just living with the misarranged ListView item icons.
I don't know whether this stems from changes in the Common Controls that the VB6 controls were never updated to handle or from this application pushing the limits somewhere. It doesn't seem to be OS-dependent but I haven't tried testing on pre-XP systems yet. Doesn't really matter in the end - it is what it is.
Deliver it as a "Known Bug" and go back and fix it later.
Deliver it as a "Known Bug" and go back and fix it later.
-tg
Yeah, that seems to be what is done in the industry.
when you quote a post could you please do it via the "Reply With Quote" button or if it multiple post click the "''+" button then "Reply With Quote" button.
If this thread is finished with please mark it "Resolved" by selecting "Mark thread resolved" from the "Thread tools" drop-down menu. https://get.cryptobrowser.site/30/4111672
Private Sub Command1_Click()
Dim I As Long
'(Re)populate ListView1 with a new set of items with new images and text.
PopulateImageList1
With ListView1.ListItems
For I = 1 To 10
.Add , , MakeName(), Int(2 * Rnd()) + 1
ListView1.LabelWrap = False: ListView1.LabelWrap = True
Next
End With
End Sub
I am sure you can measure some performance issue but I am pretty sure you can't see it.