Difference between revisions of "Tutorial:LuaFormListView"

From Cheat Engine
Jump to navigation Jump to search
(Replaced content with '<span style="font-size:25px;color:red">Sorry! Content not available.</span>')
Line 1: Line 1:
<!-- Tutorial:LuaFormListView -->
+
<span style="font-size:25px;color:red">Sorry! Content not available.</span>
[[Category:Tutorial]]
 
[[Category:Lua]]
 
{{DISPLAYTITLE:Tutorial - Using a ListView}}
 
 
 
This will be concentrating on the '''ListView''' control, check out
 
[[Tutorial:LuaFormGUI|this tutorial]] to learn more about working with
 
forms in general.
 
 
 
== 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. 
 
 
 
[[File:TutorialListViewAdd.png||Add ListView|border]]
 
 
 
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:
 
 
 
[[File:TutorialListViewProps.png||ListView ReadOnly, RowSelect, and ViewStyle|border]]
 
 
 
=== 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.
 
 
 
[[File:TutorialListViewAddOneCol.png||ListView Add One Column|border]]
 
 
 
=== 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:
 
 
 
<pre>local items = UDF1.CEListView1.Items
 
items.Clear()
 
local item = items.Add()
 
item.Caption = "First item"
 
item = items.Add()
 
item.Caption = "Second item"</pre>
 
 
 
That will clear the list and add a couple of items that you can see:
 
 
 
[[File:TutorialListViewOneCol.png||ListView Sample One Column Values|border]]
 
 
 
== 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'''.
 
 
 
[[File:TutorialListViewAddMoreColumns.png||ListView Add More Columns|border]]
 
 
 
=== 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 '''<nowiki>table.concat(<table>,'\n')</nowiki>''' to do this for a LUA
 
array and it'll call '''tostring()''' on the values.
 
 
 
<pre>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')</pre>
 
 
 
[[File:TutorialListViewMoreColumnsData.png||ListView Add More Columns|border]]
 
 
 
== 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:
 
 
 
<pre>-- 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</pre>
 
 
 
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 '''<nowiki>list[index].data</nowiki>''' 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.
 
 
 
[[File:TutorialListViewOwnerData.png||ListView Add More Columns|border]]
 
 
 
=== 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.
 
 
 
<pre style="background-color: #ff4655; color: white; white-space: pre-wrap">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.</pre>
 
 
 
=== OnCustomDrawItem ===
 
 
 
<pre>CustomDrawItem(Sender, Item, State)</pre>
 
 
 
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:
 
 
 
<pre>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</pre>
 
 
 
[[File:TutorialLuaCustomDrawItem.png||Drawing odd rows blue bg, even red fg|border]]
 
 
 
 
 
=== 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.
 
 
 
<pre>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</pre>
 
 
 
[[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 16:07, 16 March 2019

Sorry! Content not available.