PDA

View Full Version : Here's Code for DotNet ListView with Scrollable Drag and Drop and Insertion Line


Jon-Huhn
02-02-2008, 12:38 AM
I've just finished working out the details of a DotNet ListView with scrollable drag and drop for reordering the items (including an insertion line indicating where the items will be dropped).

This is probably child's play for those of you experienced in DotNet stuff, but for those out there who are just as confused as myself about DotNet, I hope this template will help get you started.

I'm sorry the code is so lengthy. If for some reason anyone out there does actually read through it, I would love feedback for how to make it better and more concise. ;)

Just evaluate the script and the ListView will launch automatically.

Zipped .ms file:

http://www.jonhuhn.com/maxscript/dotnetlistviewtemplate/DotNet_ListView_Template.zip

Or copy and paste:


-- DotNet ListView with Scrollable Drag N'Drop and Insertion Mark when dragging
---------------------------------------------------------------------------------------------
try(DestroyDialog MyListViewRoll)catch()
rollout MyListViewRoll "ListView Template with Scrollable Drag 'N Drop" (
-- Variables and functions
local dragDropEffect=dotNetClass "System.Windows.Forms.DragDropEffects" -- Drag and drop effects
local setupListView, dotNetGraphics, dotNetPen, dotNetLastDrawnRect, lastMousedItem, clickedItem, dontErase=false

-- This is array holds the content for our Listview items. An oversimplification, probably.
local lvItems=for x=1 to 20 collect "Item "+x as string

dotNetControl lv "System.Windows.Forms.ListView" width:290 height:160 pos:[20,20]

on MyListViewRoll open do setupListView()

-- Returns an array of item numbers representing the ListView items that are selected. ListView indices are zero-based, remember.
fn getSelection = for x=0 to lv.selectedIndices.count-1 collect lv.selectedIndices.item[x]

-- Given an array of items numbers, this selects those items. ListView indices are zero-based, remember
fn setSelection itemsToSelect_zeroBased = (
for itemNum_zeroBased=0 to lv.items.count-1 do (
local currItem=lv.items.item[itemNum_zeroBased]
if findItem itemsToSelect_zeroBased itemNum_zeroBased>0 then currItem.selected=true
else currItem.selected=false
)
)

-- This function reorders the content array and then tells the ListView to redraw itself based on the new array.
fn reorderItems selectedIndices targetIndex clickedIndex = (

if findItem selectedIndices targetIndex >0 then return() -- if the target is one of the selected items, then leave

local asOneBased=1, asZeroBased=-1 -- used to convert zero-based and one-based arrays back and forth

-- If we're dragging the items downward then we're going to bump the target index by one so that the content is always dropped on the opposite
-- side from where you're dragging from
if clickedIndex<targetIndex then targetIndex+=1
while (findItem selectedIndices targetIndex >0) do targetIndex+=1 -- if we've just made a selected item one of the targets, keep bumping the target until it's not selected

local origItems=for x =1 to lvItems.count collect lvItems[x] -- make a copy of the array
local targItem=try(origItems[targetIndex+asOneBased])catch(undefined) -- try to identify the content that corresponds to the target index

-- Move the selected items from the original list to a temporary list
local itemsToMove=#()
for y= selectedIndices.count to 1 by -1 do (
local indexInOrigList=selectedIndices[y]+asOneBased
append itemsToMove origItems[indexInOrigList]
deleteItem origItems indexInOrigList
)

-- If the selection was dragged to the very bottom of the listview, we can just append it to the end of the content list
if targItem==undefined then for z = itemsToMove.count to 1 by -1 do append origItems itemsToMove[z]

-- Otherwise we have to insert it
else (
-- Now that the original list has been trimmed down, the zero-based target number may be obsolete, so we need to calc what it is in trimmed down list
local targNum_oneBased=findItem origItems targItem
-- Move the selected items back into the main list, inserted at the given insertion point. We now have a nicely re-ordered list of items
for z = 1 to itemsToMove.count do insertItem itemsToMove[z] origItems (targNum_oneBased) -- insert the selected names
)
lvItems=origItems -- store this reordered array as the official content for the ListView items

setupListView() -- redraw the ListView

local newSelNums=for r in itemsToMove collect (findItem origItems r)+asZeroBased -- we want the selected items to stay selected, even after the dragging
setSelection newSelNums -- select the items
)

-- Tells Windows to redraw the portion of the screen where the insertion marker was last placed, effectively erasing it
fn eraseDragDropInsertionMark = lv.invalidate dotNetLastDrawnRect

-- Checks to see if the insertion mark needs to be erased from the old location and drawn in a new location
fn updateDragDropInsertionMark itemNum direction = (

local rect=(lv.GetItemRect itemNum) -- get the rect object of the ListView item given in the first argument

-- Here we figure out where on the Y axis to draw the insertion marker. We want to always draw it on the opposide side of the item that the cursor is coming from.
local lineY; if direction==#up then lineY=rect.top+dotNetPen.width*.5
else lineY=rect.bottom-dotNetPen.width*.5

-- If the mouse is over a different item then it was last time, we'll erase the old insertion mark and draw a new one
if itemNum!=lastMousedItem then (

-- Don't erase the line if this is the first time around after the draging has begun. Without this, the line disappears on the first item
if not dontErase then eraseDragDropInsertionMark(); dontErase=false

-- Record the item that the cursor is currently over, and draw the line
lastMousedItem=itemNum
dotNetLastDrawnRect.y=lineY-dotNetPen.width; dotNetLastDrawnRect.height=dotNetPen.width*2; dotNetLastDrawnRect.width=rect.width
dotNetGraphics.DrawLine dotNetPen rect.left lineY rect.right lineY
)
)

fn checkDragDropInsertionMarkStatus mousePos arg = (

local testItem= lv.GetItemAt mousePos.x mousePos.y -- find the ListView item under the cursor

-- If there is indeed an item under the mouse, we'll change the cursor to indicate that dropping can ocur here, and we'll update the insertion mark for added visual feedback
if testItem != undefined then (
arg.Effect=arg.AllowedEffect
local direction=#down; if clickedItem.index>testItem.index then direction=#up -- determine what direction we're dragging
updateDragDropInsertionMark testItem.index direction -- update the insertion mark

-- Otherwise there is no item under the cursor, so change the mouse cursor back to normal and erase the old insertion mark if it exists
) else (
arg.Effect=dragDropEffect.none
eraseDragDropInsertionMark()
)
)

-- If the mouse leaves the ListView area while dragging, we need to erase the insertion mark that's left behind
on lv DragLeave e do eraseDragDropInsertionMark()

-- Called when the dragging has just begun
on lv ItemDrag arg do
(
dotNetGraphics=lv.createGraphics() -- create a DotNet Graphics object
local col=DotNetClass "System.Drawing.Color"; col=col.FromArgb 150 150 160 -- create a color for the insertion line
dotNetPen=dotNetObject "System.Drawing.Pen" col 3 -- create a DotNet Pen object and optionally specify its color and width
dotNetLastDrawnRect=dotNetObject "System.Drawing.Rectangle" 0 0 0 0 -- this rect is what will keep track of what region needs to be erased
clickedItem=arg.item -- the item that was just clicked on and is now being dragged. I think this can only be one item (even if multiple are selected).

dontErase=true -- this flag keeps the insertion line from being erased initially when the dragging begins after the cursor is outside of the item that was clicked on
lv.doDragDrop arg.item dragDropEffect.Copy -- this actually starts the drag and drop process
)

-- Called repeatedly while dragging. It's here we call the function to update the insertion mark, and we also take care of scrolling
on lv DragOver arg do
(
local pos=lv.PointToClient lv.MousePosition
checkDragDropInsertionMarkStatus pos arg

-- Scroll if necessary
local scrollAmount=0
local topIndex=lv.topItem.index
if pos.y<lv.top+20 then scrollAmount=-1 -- detect if the cursor is too close to top or bottom
if pos.y>lv.bottom-12 then scrollAmount=1

if scrollAmount!=0 then (
eraseDragDropInsertionMark() -- must do this before the topItem is changed, otherwise some times line won't be erased
topIndex+=scrollAmount
if topIndex<0 then topIndex=0
else if topIndex>lv.items.count-1 then topIndex=lv.items.count-1
lv.topItem=lv.items.item[topIndex]
)
)

-- Called right after the dragging has stopped (ie the item was dropped on another item)
on lv DragDrop arg do
(
-- Find the item that the cursor was over when the drag content was dropped
local mousePos=lv.PointToClient lv.MousePosition
local dropItem=lv.GetItemAt mousePos.x mousePos.y

local selectedNums_zeroBased=getSelection() -- find out what items in the listview are selected... this is what's really being dragged

-- Now that we know what items are selected (which will serve as our dragged items), which item was originally clicked on (which is what DotNet sees as our dragged item),
-- and what item the content was dropped on, we can reorder our listview items appropriately.
reorderItems selectedNums_zeroBased dropItem.index clickedItem.index

dotNetGraphics.dispose(); dotNetGraphics=undefined -- release these DotNet items for garbage collection
dotNetPen.dispose(); dotNetPen=undefined

eraseDragDropInsertionMark() -- If the user canceled the drop, we need to clean up the insertion line
dotNetLastDrawnRect=undefined
)

fn setupListView = (

local topItemNum=0; if lv.items.count>0 then topItemNum=lv.TopItem.index -- record the scroll position so we can use it later

lv.clear() -- wipe out the contents of the entire listview so that we're redrawing it from scratch each time this function is called

lv.View = (dotNetClass "System.Windows.Forms.View").Details -- this is what allows the grid-like format to be used
lv.fullRowSelect = true -- When item is clicked, all columns in that item are selected
lv.gridLines = false-- turn off the grid lines
lv.HideSelection=false -- When this ListView loses the focus, it will still show what's selected
lv.BorderStyle=lv.BorderStyle.FixedSingle -- make the border a flat solid color instead of the Windows 3D look
lv.HeaderStyle=lv.HeaderStyle.Nonclickable -- Flattens the headers a bit (although they're still somewhat 3D) and keeps them from being clickable
lv.backColor=lv.backColor.FromArgb 225 225 225 -- Soften the background intensity a bit

lv.allowDrop=true -- required in order to implement DotNet drag and drop functionality

lv.Columns.add "Column One" 80 -- create a couple of columns and optionally specify their width
lv.Columns.add "Column Two" 80

-- Create the items that will go in the list view
theRange = #()
for x=1 to lvItems.count do ( li = dotNetObject "System.Windows.Forms.ListViewItem" lvItems[x]; append theRange li )

lv.Items.AddRange theRange -- and then put them there

if lv.items.count>0 then lv.TopItem=lv.items.item[topItemNum] -- set the scroll position to what it was before the redraw
)

)
CreateDialog MyListViewRoll width:330 height:210

Gravey
02-02-2008, 08:32 AM
thanks for sharing. I havn't had the chance to use dotnet yet unfortunatly since we're still on max 8 at work. We'll be getting 2008 soon though and i'm sure this will come in handy and help to get me started on dotnet. Thanks again!

RustyKnight
02-03-2008, 06:44 AM
That's really cool Jon! Thanks for the share!

Shane

ypuech
02-03-2008, 08:23 AM
Thanks a lot Jon!
If one day I need to use a .NET ListView and its advanced features it will be easier and faster to develop the script.

bloggs
02-03-2008, 03:50 PM
sorry for the newbie question but i'm abit clueless with this. whats DotNet supposed to be able to do in Max eg whats the main benifits?

cheers

john

Jon-Huhn
02-03-2008, 04:32 PM
I'm new to DotNet myself, so I can't give a comprehensive explanation, but I can tell you that it allows for controls to be set up in your Maxscript UIs that couldn't be done otherwise. For instance, Maxscript doesn't have a ListView control natively, so if you want to have one you need to use DotNet (ActiveX can do it too, but you should avoid developing ActiveX content from now on since it may not work in the 64 bit versions of Windows - see the topic "DotNet in Maxscript" in the Maxscript help docs).

In addition to providing UI controls that Maxscript doesn't natively provide, DotNet many times also provides more control over a given UI control. In other words, it provides more ways to customize the look and behavior of a given UI control, in ways that a native Maxscript UI control may not be able to.

For instance, for a tool I was making I wanted to make a "preview" button that when held down, would do things to the scene, but when it was released, it would return the scene to the way it was. A Maxscript Button UI control didn't allow for checking when a button was pressed down and up, only up. So I used a DotNet button to make that work for me.

RustyKnight
02-03-2008, 09:46 PM
That's some good (and probably the most significant) points Jon.

I'd also add that it provides a means by which you can write native code (ie a Windows DLL) without having to go directly through the max SDK, so you can extend some functionality of your solution...now you won't get access to all the juicy max stuff, but you could, for example, implement math routines which would take to long to calculate in maxscript, do processor intesive image manipulation and the like...but this is really the deep end of the discussion.

DotNet also provides a reasonable API/toolkit in it's own right. Some of the file handerling and XML API's are really quite cool and go WELL beyond what maxscript provides nativly.

It also provides a "common" library, which activeX can't. So, for example, if I know you have dotnet installed, I know I can use particular classes and libraries without fear of my script dying in a blaze of glory.

I hope that has confused the discussion further ;)

Shane

bloggs
02-03-2008, 10:02 PM
hey thanks Guys. yeah i have been reading abit now you have sparked my interest. how flexable would it be to repeat repetative tasks in max? epecially for those things that dont have maxscript acess?

cheers

john

RustyKnight
02-03-2008, 10:13 PM
hey thanks Guys. yeah i have been reading abit now you have sparked my interest. how flexable would it be to repeat repetative tasks in max? epecially for those things that dont have maxscript acess?

cheers

john1. It depends on what you want to achieve and 2. How good your coding skills are, you need to know, at least c# or c++ to get the most benifit

Shane

bloggs
02-03-2008, 10:30 PM
well i guess thats afew more years away for me. just maxscript and python... i'd love to dive into c++ but unfortunatly i dont have the time. and having a good coder to ask the big question would be need for me to get anywhere. ahh well good to know. i cans see what the benifits are.

cheers

john

EverZen
02-04-2008, 10:15 AM
This is exactly the sort of example that is going to all people to access and start to understand this .NET voodoo. I am certainly at the stage where I want to start incorporating it, and this sort of example is going to help so so much.

Thanks again :)

Rich

Tesctassa
02-04-2008, 10:34 AM
Reeeeeeally useful! Thanks a lot!

:P

CGTalk Moderation
02-04-2008, 10:34 AM
This thread has been automatically closed as it remained inactive for 12 months. If you wish to continue the discussion, please create a new thread in the appropriate forum.