Difference between revisions of "Tutorial:LuaFormListView"
(→Using OnCustomDrawItem, OnCustomDrawSubItem) |
|||
Line 201: | Line 201: | ||
[[File:TutorialLuaCustomDrawSubItem.png||Style Columns|border]] | [[File:TutorialLuaCustomDrawSubItem.png||Style Columns|border]] | ||
+ | |||
+ | == Selecting Items == | ||
+ | |||
+ | Edit the '''CEListView1''' properties in the designer and uncheck '''HideSelection''' to change it to False. | ||
+ | This maintains the selection, though it looks grayed out when another control is selected. | ||
+ | |||
+ | You should already have '''RowSelect''' set to True from earlier. When this is false, only the first column of the row is selected. | ||
+ | I think that looks wonky, I don't know why you would want this. You can't even select other columns... | ||
+ | |||
+ | Finally, '''MultiSelect' should be false. When this is set, you have to check each individual item (UDF1.CEListView1.Items[x].Selected) to see if it's selected. On my computer that's about 1,000 items per second, so I wouldn't want to do that for more than 50 items or so. When allowing only a single item to be selected you can use UDF1.CEListView1.getItemIndes() to get the index of the selected item (or -1 if none is selected). | ||
+ | |||
+ | === OnSelectItem === | ||
+ | |||
+ | There is an '''OnSelectItem''' event that is a little weird. The signature looks like this: | ||
+ | |||
+ | <pre>function CEListView1SelectItem(sender, listitem, selected) | ||
+ | print('SelectItem!', tostring(listitem.Index), tostring(selected)) | ||
+ | end</pre> | ||
+ | |||
+ | It is called with listitem.index being the selected index and selected=true, but it is also called with listitem.index=-1 and selected=false to clear the selection. With single-select it is always called twice: once to clear the selection (-1,false), then with the newly selected index and true. With multi-select it works the same way when selecting single rows, but when using CTRL+CLICK to select additional rows you don't get the -1,false to clear the selection. However when you use SHIFT+CLICK to select a range it ''only'' gives you that clear message. Pretty useless... | ||
+ | |||
+ | === Finding with Multiselect=true === | ||
+ | |||
+ | Again, the performance is horrible so don't use it with big lists (> 100 items), and if you do you should probably set a limit | ||
+ | in code so it won't access too many items. | ||
+ | |||
+ | <pre>local items = UDF1.CEListView1.Items | ||
+ | local selected = {} | ||
+ | for i=0,math.min(100,#items.count-1) do | ||
+ | if items[i].Selected then table.insert(selected, i) end | ||
+ | end</pre> | ||
+ | |||
+ | === Finding with single select === | ||
+ | |||
+ | You can use either of these to get the selected index, which is 0-based and will be -1 if no item is selected: | ||
+ | |||
+ | <pre>UDF1.CEListView1.Selected.Index | ||
+ | UDF1.CEListView1.getItemIndex()</pre> | ||
+ | |||
+ | |||
+ | == Other Events == | ||
+ | |||
+ | === DblClick === | ||
+ | |||
+ | I like using '''DblClick''' for doing something if you have one main thing to do with an item, it preserves the 'single-click to select' ux and is pretty common. | ||
+ | |||
+ | Double-click in it in the Events tab of the object inspector to create a template or edit the value to be an existing function. | ||
+ | You can see what it does with this code: | ||
+ | |||
+ | <pre>function CEListView1DblClick(sender) | ||
+ | print('You double-clicked with selected index '..tostring(sender.getItemIndex())) | ||
+ | end</pre> | ||
+ | |||
+ | === KeyDown === | ||
+ | |||
+ | Double-click the space by this event in the Events tab of the object inspector to create a template, or set the value to an | ||
+ | existing function. The 'key' parameter is the key code, you can see a list in '''Defines.lua''' in the directory where | ||
+ | Cheat Engine is installed. This handler will print a message to the lua console only when the 'RETURN' key is pressed. | ||
+ | |||
+ | <pre>function CEListView1KeyDown(sender, key) | ||
+ | if key == VK_RETURN then | ||
+ | print('Hit key "'..tostring(key)..'" with selected index '..tostring(sender.getItemIndex())) | ||
+ | end | ||
+ | return key | ||
+ | end</pre> | ||
+ | |||
+ | You can return other keys, but I'm not sure of the rules. Letters and Numbers seem to move the list to the top and the value you return doesn't matter. Other keys like function keys, cursor keys, return, space, PGUP (VK_PRIOR) and PGDOWN (VK_NEXT) will replace that key with what you return. For instance you could return VK_NEXT if the key is VK_PRIOR and both PGUP and PGDOWN would act like PGDOWN. |
Revision as of 09:08, 23 July 2018
This will be concentrating on the ListView control, check out this tutorial to learn more about working with forms in general.
Contents
Initial Setup
The Form
To get started create a form with Table->Create form, this generated a UDF1 form for me. Now click the ListView button and drag an area on the form to set its extends.
In the properties change the ViewStyle to vsReport, this gives us a normal view with rows and columns. Also change ReadOnly to true because we won't allow editing, and RowSelect to true so clicking will select an entire row instead of just one column value:
Adding a Column
Now let's add a column. Click on the Columns property and the '...' to the right to open the column editor and click 'Add' to add a new column. Here I set the Caption of the column to 'Message' and changed the Width from 50 to 200. You can also change the width by dragging the column separator on the design form.
Add Items
Now close design mode by clicking the X on the top right of the toolbar window and the form will display in normal mode. Execute this code:
local items = UDF1.CEListView1.Items items.Clear() local item = items.Add() item.Caption = "First item" item = items.Add() item.Caption = "Second item"
That will clear the list and add a couple of items that you can see:
Adding More Columns
Now I'll go back to the properties of CEListView1 and click on the '...' in the Columns property to open the column editor and click Add to add a couple of more columns and edit their Caption properties to be Extra and Clock.
Add Data For New Columns
A TListItem has the value for the first column in the Caption property. The data for other columns is stored in the SubItems property as a Strings object. I find it easiest to update by setting the 'Text' property to a list of strings with linebreaks. You can use table.concat(<table>,'\n') to do this for a LUA array and it'll call tostring() on the values.
local items = UDF1.CEListView1.Items items.Clear() local item = items.Add() item.Caption = "First item" item.SubItems.text = "Hello\n"..tostring(os.clock()) item = items.Add() item.Caption = "Second item" item.SubItems.text = table.concat({"World",os.clock()}, '\n')
Using OwnerData
What if you have a LOT of items you want to display? There's overhead with creating the items and I couldn't get BeginUpdate/EndUpdate to work. That's where the OwnerData property comes in! Check the OwnerData property of the ListView to make it True. Change the Object Inspector tab from Properties to Events and double-click on the OnData property to create an event handler. Now I'll change the table script to look like this and run it:
-- items list = {} for i=1,1000000 do local item = { message = string.format('Message %d', i), data = math.sqrt(i) } table.insert(list, item) end -- need to set the Items.Count property so that it knows how many rows there -- are in total UDF1.CEListView1.Items.Count = #list -- definition created by double-clicking OnData event handler function CEListView1Data(sender, listitem) -- use + 1 because listitem.Index is 0-based local d = list[listitem.Index + 1] if not d then return end listitem.Caption = d.message -- first column -- other columns have sqrt of data property and CURRENT clock -- when this event was called local others = {string.format('%0.3f', math.sqrt(d.data)), os.clock()} listitem.SubItems.text = table.concat(others, '\n') end
So we set UDF1.CEListView1.Items.Count to the number of items the ListView should display. The OnData event handler will be called for each displayed row. When the displayed row changes, the method is called again. This way it is called just for the visible rows in the control.
You can tell it's getting called by scrolling down one row then back up, the 'Clock' column value will change. If you resize the form (and have the anchors set so the ListView will resize) then they all change. You can also see it when selecting or deselecting rows because it needs to be redrawn.
The list[index].data property is actually the square root of the index and the handler sets the actual displayed value to the square root of that, so you can see Message 16 has an Extra value of 2.000.
Aside on syncing with lua
If you see something you don't expect, it could be that the event handlers are using old functions. Try these steps:
- Close down the designer and the form
- Run your script that creates the functions used by the form events
- Click Table=>Resynchronize forms with lua
Using OnCustomDrawItem, OnCustomDrawSubItem
If you want to change the style of items you can use the OnCustomDrawItem and OnCustomDrawSubItem events. These can alter the styles as shown here and CE will draw the text as normal if you return true, but I think they can also be used to do other drawing.
Warning! I have CE become unresponsive sometimes, I think it's best to close the editor when saving changes to these methods. You can still leave the form open to see changes, just not the editor. I think it is changing the item array and item count with the custom data most likely.
OnCustomDrawItem
CustomDrawItem(Sender, Item, State)
This is called for the first column that displays the Caption. If you change a style here it will be set for other columns as well. Sender is the ListView control, Item is the ListItem, and I'm not sure what State is. Here's a sample that will draw odd rows (0-based) with a blue background:
function CEListView1CustomDrawItem(Sender, Item, State) if (Item.Index % 2) == 1 then Sender.Canvas.Brush.Color = 0xffe0e0 -- odd rows blue bg (0-based) else Sender.Canvas.Brush.Color = 0xffffff -- other rows white bg red fg Sender.Canvas.Font.Color = 0x4040ff end return true --return true for DefaultDraw end
OnCustomDrawSubItem
This is called for columns other than the first. You should have OnCustomDrawItem set as well to handle the first column, and I think it's best just to have it delegate to this method. The value are the same except for a new SubItem parameter that is the zero-based column index.
function CEListView1CustomDrawSubItem(Sender, Item, SubItem, State) if (Item.Index % 2) == 1 then Sender.Canvas.Brush.Color = 0xffe0e0 -- odd rows blue bg (0-based) if SubItem == 1 then Sender.Canvas.Font.Color = 0x8f8f8f end -- gray middle column else Sender.Canvas.Brush.Color = 0xffffff -- odd rows white bg (0-based) if SubItem == 2 then Sender.Canvas.Font.Color = 0xffff40 -- cyan right column else Sender.Canvas.Font.Color = 0x2020ff -- red other columns end end return true -- return true for DefaultDraw end function CEListView1CustomDrawItem(Sender, Item, State) return CEListView1CustomDrawSubItem(Sender, Item, 0, State) end
Selecting Items
Edit the CEListView1 properties in the designer and uncheck HideSelection to change it to False. This maintains the selection, though it looks grayed out when another control is selected.
You should already have RowSelect set to True from earlier. When this is false, only the first column of the row is selected. I think that looks wonky, I don't know why you would want this. You can't even select other columns...
Finally, MultiSelect' should be false. When this is set, you have to check each individual item (UDF1.CEListView1.Items[x].Selected) to see if it's selected. On my computer that's about 1,000 items per second, so I wouldn't want to do that for more than 50 items or so. When allowing only a single item to be selected you can use UDF1.CEListView1.getItemIndes() to get the index of the selected item (or -1 if none is selected).
OnSelectItem
There is an OnSelectItem event that is a little weird. The signature looks like this:
function CEListView1SelectItem(sender, listitem, selected) print('SelectItem!', tostring(listitem.Index), tostring(selected)) end
It is called with listitem.index being the selected index and selected=true, but it is also called with listitem.index=-1 and selected=false to clear the selection. With single-select it is always called twice: once to clear the selection (-1,false), then with the newly selected index and true. With multi-select it works the same way when selecting single rows, but when using CTRL+CLICK to select additional rows you don't get the -1,false to clear the selection. However when you use SHIFT+CLICK to select a range it only gives you that clear message. Pretty useless...
Finding with Multiselect=true
Again, the performance is horrible so don't use it with big lists (> 100 items), and if you do you should probably set a limit in code so it won't access too many items.
local items = UDF1.CEListView1.Items local selected = {} for i=0,math.min(100,#items.count-1) do if items[i].Selected then table.insert(selected, i) end end
Finding with single select
You can use either of these to get the selected index, which is 0-based and will be -1 if no item is selected:
UDF1.CEListView1.Selected.Index UDF1.CEListView1.getItemIndex()
Other Events
DblClick
I like using DblClick for doing something if you have one main thing to do with an item, it preserves the 'single-click to select' ux and is pretty common.
Double-click in it in the Events tab of the object inspector to create a template or edit the value to be an existing function. You can see what it does with this code:
function CEListView1DblClick(sender) print('You double-clicked with selected index '..tostring(sender.getItemIndex())) end
KeyDown
Double-click the space by this event in the Events tab of the object inspector to create a template, or set the value to an existing function. The 'key' parameter is the key code, you can see a list in Defines.lua in the directory where Cheat Engine is installed. This handler will print a message to the lua console only when the 'RETURN' key is pressed.
function CEListView1KeyDown(sender, key) if key == VK_RETURN then print('Hit key "'..tostring(key)..'" with selected index '..tostring(sender.getItemIndex())) end return key end
You can return other keys, but I'm not sure of the rules. Letters and Numbers seem to move the list to the top and the value you return doesn't matter. Other keys like function keys, cursor keys, return, space, PGUP (VK_PRIOR) and PGDOWN (VK_NEXT) will replace that key with what you return. For instance you could return VK_NEXT if the key is VK_PRIOR and both PGUP and PGDOWN would act like PGDOWN.