Code archives/BlitzPlus Gui/Multi-Column List Box

This code has been declared by its author to be Public Domain code.

Download source code

Multi-Column List Box by Ghost Dancer2008
Multi-Column List Box with sort (Windows only). I've modified Ziltches code to make it OO structured and also added my own sort functionality (clicking column heading will alternate between ascending and decending sort). Currently no support for multiple selected items but hassle me and I'm sure I'll do it.
'******************************************************************************
'Multi-column list box with sort V1.2
'Original code: Ziltch 29 August 2006.
'Modified by Ghost Dancer 06 August 2008
'You can use this code if you credit Ziltch & Ghost Dancer
'
'V1.2 update
'	- Added sortEnable & sortDisable methods.
'	- Fixed sorting bug.
'	- Added update method which can be called on EVENT_WINDOWSIZE to reset first column width.
'	- Renamed some methods.
'
'V1.1 updates
'	- Fixed to work in Max 1.30.
'	- TListView now extends TProxyGadget.
'	- Is now unicode compliant.
'	- Remembers the selected item after a sort.
'******************************************************************************

'******************************************************************************
'example usage
'******************************************************************************
Strict

Import MaxGUI.Drivers

'set up
Local window:TGadget = CreateWindow("Multi-Column List Example", 100, 100, 320, 250, Null, WINDOW_RESIZABLE|WINDOW_TITLEBAR|WINDOW_CLIENTCOORDS)
Local listView:TListView = TListView.Create(2, 2, 310, 150, window, "Name")
SetGadgetLayout listView, EDGE_ALIGNED, EDGE_ALIGNED, EDGE_ALIGNED, EDGE_CENTERED

'add 2nd & 3rd columns
listView.addListViewColumn("Sex", 80)
listView.addListViewColumn("Age", 80)

'add some data
listView.addListViewItem(["Simon", "Male", "34"])
listView.addListViewItem(["Jane", "Female", "29"])
listView.addListViewItem(["Peter", "Male", "38"])
listView.addListViewItem(["Sally", "Female", "44"])

'2nd list
Local listView2:TListView = TListView.Create(2, 160, 310, 80, window, "one")
SetGadgetLayout listView2, EDGE_ALIGNED, EDGE_ALIGNED, EDGE_ALIGNED, EDGE_ALIGNED

listView2.addListViewColumn("two", 80)
listView2.addListViewColumn("three", 80)

'add some data
listView2.addListViewItem(["xtest 1-1", "xtest 1-2", "xtest 1-3"])
listView2.addListViewItem(["test 2-1", "test 2-2", "test 2-3"])

Local Gadget:TGadget

'Main Loop
Repeat
	WaitEvent()
	Gadget = TGadget(EventSource())
	
	Select EventID()
		Case EVENT_WINDOWSIZE
			TListView.update
		Case EVENT_WINDOWCLOSE
			Select gadget
			Case window
				Exit
			End Select
	End Select
Forever

'print selected data
Print "Selected Item: " + listView.getSelectedItem() + ", aged " + listView.getSelectedItem(2)

End


'==============================================================================
Type TListView Extends TProxyGadget
'==============================================================================
	Const LBS_MULTICOLUMN = 512
	
	'globals are used for sorting
	Global oldListProc							'used for column heading selections
	Global gadgetList:TList = CreateList()		'global list of type instances
	
	Field curColumn								'number of columns in list
	Field columnHeading:TColumnHeading[1]		'array of column data
	
	Field listBox:TGadget						'the gadget
	Field listboxHwnd							'listbox's HWND handle
	Field sortColumns = True					'enable/disable sorting
	
	
	'------------------------------------------------------------------------------
	Function create:TListView(x, y, w, h, parent:TGadget, heading$ = "", width = 100)
	'heading & width are for first column
	'------------------------------------------------------------------------------
		Local newListView:TListView = New TListView 
		
		newListView.listBox = CreateListBox(x, y, w, h, parent)
		newListView.SetProxy newListView.listBox
		
		Local TempCanvas:TGadget = CreateCanvas(0, 0, 0, 0, parent)
		
		newListView.listboxHwnd = QueryGadget(newListView.listBox, QUERY_HWND)
		
		oldListProc = SetWindowLongW(newListView.listboxHwnd, GWL_WNDPROC, Int(Byte Ptr NewListProc))
		
		newListView.setHeading(0, heading$, width)
		
		'store the gadget in global list...
		gadgetList.AddLast newListView
		
		'and also return it for standard OO usage
		Return newListView
	End Function
	
	
	'------------------------------------------------------------------------------
	Method setHeading(column, heading$, width, action = LVM_SETCOLUMNW)
	'------------------------------------------------------------------------------
		Local col:LVCOLUMNW = New LVCOLUMNW
		
		columnHeading[column] = New TColumnHeading
		columnHeading[column].width = width
		
		If columnHeading[Column].width = 0 Then
			Col.mask = LVCF_TEXT| LVCF_FMT 
		Else
			Col.mask = LVCF_TEXT| LVCF_FMT | LVCF_WIDTH
			col.cx   = columnHeading[Column].width
		End If
		
		col.pszText = heading$.ToWString()
		
		Local ListBoxstyle = GetWindowLongW(ListboxHwnd , GWL_STYLE)
		
		If (ListBoxstyle &  LVS_NOCOLUMNHEADER ) Then
			ListBoxstyle  = ListBoxstyle  ~LVS_NOCOLUMNHEADER 
			If ListBoxstyle & LVS_EDITLABELS=0 Then ListBoxstyle  = ListBoxstyle  | LVS_EDITLABELS
			SetWindowLongW(ListboxHwnd , GWL_STYLE,  ListBoxstyle )  'change the style so that we have headings
		End If
		
		SendMessageW(ListboxHwnd, action, Column, Int(Byte Ptr Col))
		
		columnWidth
	End Method
	
	
	'------------------------------------------------------------------------------
	Method addListViewColumn(heading$, width)
	'------------------------------------------------------------------------------
		curColumn:+ 1
		
		columnHeading = columnHeading[..curColumn+1]
		
		setHeading curColumn, heading, width, LVM_INSERTCOLUMNW
	End Method
	
	
	'------------------------------------------------------------------------------
	Method addListViewItem(text$[])
	'add full row from array
	'------------------------------------------------------------------------------
		Local curRow = CountGadgetItems(listBox)
		
		'add first column & reset width
		AddGadgetItem(listBox, text$[0])
		columnWidth
		
		For Local column = 1 To text$.Length - 1
			Local ListboxHwnd = QueryGadget(listBox, QUERY_HWND)
			Local ColItem:LVITEMW  = New LVITEMW
			
			ColItem.mask = LVIF_TEXT
			ColItem.iSubItem = column
			ColItem.iItem = curRow
			ColItem.pszText = Text$[column].ToWString()
			ColItem.cchTextMax =  Text$[column].Length + 1
			SendMessageW( ListboxHwnd, LVM_SETITEMW ,0, Int(Byte Ptr ColItem))
			ColItem = Null
		Next
	End Method
	
	
	'------------------------------------------------------------------------------
	Method getListViewItem:String(Row, Column)
	'------------------------------------------------------------------------------
		If ListboxHwnd Then
			Local Ans$
			Local TextStorage:Short[1024]
			Local ColItem:LVITEMW  = New LVITEMW
			
			ColItem.mask = LVIF_TEXT
			ColItem.iSubItem = Column
			ColItem.iItem = Row
			ColItem.pszText = TextStorage
			ColItem.cchTextMax =  1024 
			SendMessageW( ListboxHwnd,LVM_GETITEMW,0,Int(Byte Ptr ColItem))
			
			If ColItem.pszText Then
				Ans$=String.FromWString(ColItem.pszText)
				ColItem=Null
			End If
			
			Return Trim(Ans$)
		End If
	End Method
	
	
	'------------------------------------------------------------------------------
	Method setListViewItem(Text:String, Row, Column = 0)
	'set text in a specific row, column
	'------------------------------------------------------------------------------
		If Column = 0 Then listBox.items[Row].Text = text
		
		If ListboxHwnd Then
			Local ColItem:LVITEMW  = New LVITEMW
			ColItem.mask = LVIF_TEXT
			ColItem.iSubItem = Column
			ColItem.iItem = Row
			ColItem.pszText = Text.ToWString()
			ColItem.cchTextMax =  Len(Text) 
			
			Return SendMessageW( ListboxHwnd,LVM_SETITEMW,0,Int(Byte Ptr ColItem))
		End If
	End Method
	
	
	'------------------------------------------------------------------------------
	Method columnWidth(Column = 0)
	'------------------------------------------------------------------------------
		Local col:LVCOLUMNW = New LVCOLUMNW
		
		Col.mask = LVCF_WIDTH
		col.cx   = columnHeading[Column].width
		SendMessageW(ListboxHwnd,LVM_SETCOLUMNW,Column,Int(Byte Ptr Col))
	End Method
	
	
	'------------------------------------------------------------------------------
	Method listBoxetMultiSelect()
	'------------------------------------------------------------------------------
		Local ListBoxstyle = GetWindowLongW(ListboxHwnd , GWL_STYLE)
		
		If  (ListBoxstyle & LVS_SINGLESEL) = LVS_SINGLESEL Then
			Return SetWindowLongW(ListboxHwnd , GWL_STYLE,  ListBoxstyle  ~ LVS_SINGLESEL )  'change the style 		
		End If
	End Method
	
	
	'------------------------------------------------------------------------------
	Method getSelectedItem$(col = 0)
	'------------------------------------------------------------------------------
		If SelectedGadgetItem(listBox) >= 0 Then
			Return getListViewItem(SelectedGadgetItem(listBox), col)
		End If
	End Method
	
	
	'------------------------------------------------------------------------------
	Method sort(sortColumn)
	'------------------------------------------------------------------------------
		If sortColumns Then
			Local itemCount = CountGadgetItems(listBox)
			
			If itemCount Then
				Local c, r, sortCount
				Local selectedItem, selectedItemNew = -1
				Local newList:TList = CreateList()
				Local rowCount = CountGadgetItems(listBox)	'total number of rows/items in list
				
				'store sorted rows in temp list
				Repeat
					Local rowNum = 0, rowText$
					If columnHeading[sortColumn].sortDir = 1 Then rowText$ = "zzzzzz"
					
					'find next row in sequence
					For r = 0 To CountGadgetItems(listBox) - 1
						If getListViewItem(r, sortColumn).ToLower() <= rowText$ = columnHeading[sortColumn].sortDir Then
							rowText$ = getListViewItem(r, sortColumn).ToLower()
							rowNum = r
						End If
					Next
					
					'update selected index
					If selectedItemNew = -1 Then
						 selectedItem = SelectedGadgetItem(listBox)
						If selectedItem = rowNum Then selectedItemNew = sortCount
					End If
					
					'create an temp array to store row data
					Local listRow$[curColumn+1]
					
					'copy this row to new list
					For c = 0 To curColumn
						listRow$[c] = getListViewItem(rowNum, c)
					Next
					
					newList.AddLast listRow$
					
					'remove row from original list & update counter
					listBox.RemoveItem rowNum
					sortCount:+ 1
				Until sortCount = rowCount
				
				'copy sorted data to new list
				For Local row$[] = EachIn newList
					addListViewItem(row$)
				Next
				
				'change sort direction for this column
				If columnHeading[sortColumn].sortDir = 1 Then
					columnHeading[sortColumn].sortDir = 0
				Else
					columnHeading[sortColumn].sortDir = 1
				End If
				
				'reselect item
				If selectedItemNew >= 0 Then SelectGadgetItem listBox, selectedItemNew
			End If
		End If
	End Method
	
	
	'------------------------------------------------------------------------------
	Method sortEnable()
	'------------------------------------------------------------------------------
		sortColumns = True
	End Method
	
	
	'------------------------------------------------------------------------------
	Method sortDisable()
	'------------------------------------------------------------------------------
		sortColumns = False
	End Method
	
	
	'------------------------------------------------------------------------------
	Function update()
	'call on EVENT_WINDOWSIZE to reset width of first column
	'------------------------------------------------------------------------------
		For Local newListView:TListView = EachIn gadgetList
			newListView.columnWidth
		Next
	End Function
	
	
	'------------------------------------------------------------------------------
	Function NewListProc:Int(hWnd:Int, Msg:Int, wParam:Int, lParam) "win32"
	'------------------------------------------------------------------------------
		Const HDN_ITEMCLICKW = -322
		
		If Msg = WM_NOTIFY Then
			Local NotifyMess:HD_NOTIFY = New HD_NOTIFY
			Local Tstr$
			
			MemCopy( NotifyMess, Byte Ptr lParam, SizeOf(HD_NOTIFY) )
			
			If NotifyMess.code = HDN_ITEMCLICKW Then
				'find gadget that was clicked & sort it
				For Local newListView:TListView = EachIn gadgetList
					If newListView.listboxHwnd = hWnd Then newListView.sort NotifyMess.iitem
				Next
			End If
		End If
		
		If oldListProc <>0 Then Return CallWindowProcW(Byte Ptr oldListProc, hWnd, Msg, wParam, lParam)
	End Function

End Type


'==============================================================================
Type TColumnHeading
'used by TListView
'==============================================================================
	Field width
	Field sortDir = 1
EndType


'==============================================================================
Type HD_NOTIFY 
'used by TListView
'==============================================================================
	Field hwndFrom
	Field idFrom
	Field code
	Field iItem
	Field iButton
	Field pitem
EndType

Comments

SebHoll2008
Nice work! A few comments:

1) You might like TListView to extend TProxyGadget, setting the listview as the proxy gadget using the SetProxy() method in TListView.Create(). That way you can use the standard MaxGUI commands with it.

2) You may want to made it unicode compliant, by using the wide-char window messages (such as LVM_SETCOLUMNW instead of LVM_SETCOLUMNA, or SetWindowLongW instead of SetWindowLongA) and by using .ToWString() / $w when accessing the Windows API.

3) You'll need to add Import MaxGUI.Drivers to the top of the code, just under Strict, if you want it to compile in BlitzMax v1.30.


Ked2008
This would be crazy sexy if it was extended from TProxyGadget. I'd use it all of the time!


Jesse2008
it doesn't work properly with Bmax 1.30, it freezes. It works good with 1.28 though.


Ghost Dancer2008
Thanks for the info seb. I've updated it to include all your suggestions. Not sure why, but I had to add a canvas (line 95) or it crashed when the window was moved!


SebHoll2008
Cool, but I think you've missed a few of the Unicode commands. Also, some of the type instances are already declared in Pub.Win32 (imported by MaxGUI.Drivers) so I've gone over it once myself. After having made the changes, I commented out the canvas line and it seems to work fine this end (I'm on Vista though):



Hope it helps! ;-)


degac2008
Wow! What I just need for a little office application at work! Works perfectly (the first one, the one posted by SebHoll 'locks' the window - it becomes black - and I can't do nothing else on it).
Many thanks!

PS: I don't know why I need to use a CanvasGadget, but I've modified it to use a smaller canvas (1,1,1,1) and it works. I dont' know if the size counts or not, but I think a smaller canvas occupies less memory at least.


Ked2008
I think it would be best to not reuse the CreateListBox() function. I feel like that could be causing some problems. I have no proof of that statement though.


jsp2008
(the first one, the one posted by SebHoll 'locks' the window - it becomes black - and I can't do nothing else on it).


Here the same, also WinXP pro SP2 like degac

PS: I don't know why I need to use a CanvasGadget, but I've modified it to use a smaller canvas (1,1,1,1) and it works. I dont' know if the size counts or not, but I think a smaller canvas occupies less memory at least.


Looks like size doesn't matter, works here also with:
Local TempCanvas:TGadget = CreateCanvas(0, 0, 0, 0, parent)


Ghost Dancer2008
Thanks for all your help and suggestions guys. I've updated my code to include Sebs version with the canvas added back in (size 0).

There are still a few tweeks needed on it (remembering seelction, column width) which I will have another crack at later.


Ghost Dancer2008
I think it would be best to not reuse the CreateListBox() function.


Not quite sure what you mean there Ked? Thats needed to create the gadget!


Ked2008
I'm not sure either. Forget about it. :)


Ghost Dancer2008
Code updated to V1.2:

- Added sortEnable & sortDisable methods.
- Fixed sorting bug.
- Added update method which can be called on EVENT_WINDOWSIZE to reset first column width.
- Renamed some methods.


danielos2008
Good work!
Is it possible to get all selected entries when multiselect is turned on ?

Cheers,
Daniel


xlsior2008
Currently no support for multiple selected items but hassle me and I'm sure I'll do it.


<hassle>Multi-select would be a very welcome addition to this control. </hassle>


Also, Is there any way to keep the left-most from resizing back to a narrow format entirely while resizing the window? It looks a bit odd to have the contents jump around for seemingly no reason. (It even gets narrow when you make the window larger, then pops back to its original size once you stop resizing the window)


Regular K2009


multi-select method


JoshK2010
Here's a version that uses a proxygadget and allows you to create more than one multicolumn listbox:



Code Archives Forum