Page 1 of 6

Editing cells in ListIcons (updated for Vista)

Posted: Fri May 11, 2007 2:31 am
by srod
Update: 08/07/08. Vista compatible.
Getting this to run on Vista required a couple of dirty hacks as we are doing something here which Windows (especially Vista) is set up to prevent! :) In fact, earlier version of Windows are also set up to prevent us moving the edit control reserved for editing labels (column 0) in order to edit other cells - it's just that the prevention fails! Not on Vista (with XP themes enabled) however!

I'll not bore you with the details of the evil hacks - suffice to say that I have tested on XP and Vista and everything seems okay. I am suspicious that the edit control alignment will be out on Win Server 2003, but this is easily fixed if it proves to be the case.

Code is below.

===================================


Update: 31/07/07.
ListIcon now scrolls automatically if the user double-clicks a partially visible cell to edit it etc.

===================================


Update: 11/05/07.
Added the command EditCell() to allow a cell to be edited from code.


===================================


Hi,

something I've been tinkering with for the last couple of hours, just to see if it could be done! :) and which adds yet another method of editing cells in a listicon to the collection from myself, netmaestro, Xombie, Einander,...

It works as follows.

Basically, a Windows list view control allows you to edit the labels in column zero if you set the #LVS_EDITLABLES style and, at a minimum, process the #LVN_ENDLABELEDIT message. What my library does on intercepting a double-click on a valid cell, is instruct Windows to edit the corresponding label in column 0, but before Windows has a chance to position the edit control used to actually edit the label, the library manages to grab a hold of the control and reposition it over the selected cell etc. On completion of the editing process, I just switch the text in the cell for that in the edit control etc.

Simple and quite effective. :)

It's not perfect, but then this is nothing more than a cheap and cheerful way of editing cells!

Edit ListIcon.pbi

Code: Select all

;'Edit ListIcon'.
;-----------------
;   Stephen Rodriguez.
;   Created with Purebasic 4.02 for Windows (date:  May 2007).
;   Maintained with PB 4.4.

;   Platforms:  Windows (tested on XP and Vista 32-bit).

;   Licence: DAYLike
;     (Do As You Like with it! - No Warranties!)
;     A credit to myself, whilst nice, is not absolutely necessary.

;*******************************************************************************************

;NOTES.
;------
; 1)  Register a listicon gadget to have editable cells by using the command SetListIconEditable(listID).
;     You MUST set up such listicons to have a column zero of zero width.
; 2)  Cells are made editable by intercepting double-clicks, setting the #LVS_EDITLABELS style,
;     repositioning the edit control which Windows uses to edit the labels in column zero and copying
;     the resulting text to the listicon cell.
; 3)  Cells can also be edited by means of the command EditCell().
;*******************************************************************************************

#LVM_SUBITEMHITTEST = #LVM_FIRST+57 
#LVM_GETSUBITEMRECT = #LVM_FIRST+56 
#LVM_GETHEADER = #LVM_FIRST+31
#HDI_ORDER        = $80 
#EC_RIGHTMARGIN = 2

EnableExplicit

CompilerIf Defined(HDITEM, #PB_Structure) = 0
  Structure HDITEM 
    mask.l 
    cxy.l 
    pszText.i 
    hbm.l 
    cchTextMax.l 
    fmt.l 
    lParam.i 
    iImage.l 
    iOrder.l 
  EndStructure 
CompilerEndIf

Structure _LIEdit
  listOldProc.i
  editHwnd.i
  item.i
  subitem.i
  x.i
  y.i
  cx.i
  cy.i
  osVersion.b
  blnIsXPThemes.b
EndStructure

Structure _LIEditGlobals
  osVersion.b
  blnIsXPThemes.b
EndStructure

Declare.i SetListIconEditable(listID)
Declare _LIEEditCell(*liedit._LIEdit, hWnd)
Declare.i _LIEwinProc(hWnd, uMsg, wParam, lParam)
Declare.i _LIEListProc(hWnd, uMsg, wParam, lParam)
Declare.i _LIEeditProc(hWnd, uMsg, wParam, lParam)

Global _LIEditGlobals._LIEditGlobals

;Returns zero if an error.
Procedure.i SetListIconEditable(listID)
  Protected result, parenthWnd, *mem._LIEdit, hWnd, dlv.DLLVERSIONINFO, func, lib
  ;Set globals if appropriate.
    If _LIEditGlobals\osVersion = 0
      _LIEditGlobals\osVersion = OSVersion()
      dlv\cbSize=SizeOf(DLLVERSIONINFO)
      lib=OpenLibrary(#PB_Any,"comctl32.dll")
      If lib
        func=GetFunction(lib,"DllGetVersion")
        If func
          CallFunctionFast(func, dlv)
          If dlv\dwMajorVersion >=6
            _LIEditGlobals\blnIsXPThemes = #True
          EndIf            
        EndIf
        CloseLibrary(lib)
      EndIf
    EndIf
  ;Check that listID references a valid listicon.
    If IsGadget(listID) And GadgetType(listID)=#PB_GadgetType_ListIcon
      hWnd = GadgetID(listID)
      ;Is the listicon already registered?
      If GetProp_(hWnd, "_LIEdit")=0 ;No!
        ;Allocate enough memory for a _LIEdit structure.
          *mem=AllocateMemory(SizeOf(_LIEdit))
        If *mem
          SetWindowLong_(hWnd, #GWL_STYLE, GetWindowLong_(hWnd, #GWL_STYLE)&~#LVS_EDITLABELS)
          ;Set the fields of the _LIEedit structure.
            *mem\listOldProc = SetWindowLong_(hWnd, #GWL_WNDPROC, @_LIEListProc())
          ;Store a pointer to this structure in a window property ofthe listicon.          
            SetProp_(hWnd, "_LIEdit", *mem)
          ;Subclass the parent window if not already through another listicon.
            parenthWnd=GetParent_(hWnd)
            If GetProp_(parenthWnd, "_LIEditOldProc")=0 ;No!
              SetProp_(parenthWnd, "_LIEditOldProc", SetWindowLong_(parenthWnd, #GWL_WNDPROC, @_LIEwinProc()))
            EndIf
          result=1
        EndIf
      EndIf
    EndIf
  ProcedureReturn result
EndProcedure


;Sets the specified cell to be edited.
Procedure EditCell(listID, item, subitem)
  Protected hWnd, *liedit._LIEdit, numrows, numcols
  ;Check that listID references a valid listicon.
    If IsGadget(listID) And GadgetType(listID)=#PB_GadgetType_ListIcon
      ;Check that the listicon is registered as editable.
        hWnd = GadgetID(listID)
        *liedit = GetProp_(hWnd, "_LIEdit")
        If *liedit
          ;Check parameters are in range.
            numrows = CountGadgetItems(listID)
            numcols = SendMessage_(SendMessage_(hWnd,#LVM_GETHEADER,0,0), #HDM_GETITEMCOUNT,0,0)
            If item>=0 And item < numrows And subitem>0 And subitem < numcols
              *liedit\item = item
              *liedit\subitem = subitem
              SetActiveGadget(listID)
              _LIEEditCell(*liedit, hWnd)
            EndIf
        EndIf
    EndIf
EndProcedure


Procedure _LIEEditCell(*liedit._LIEdit, hWnd)
  Protected rc.RECT, clientrc.RECT, numCols, headerWnd
  Protected Dim cols.l(0), i, blnFoundZeroColumn
  Protected hdi.HDITEM
  ;Vista themes requires a cheat because it automatically scrolls the listicon to bring the item being edited
  ;into view.
    If _LIEditGlobals\osVersion >= #PB_OS_Windows_Vista And _LIEditGlobals\blnIsXPThemes   
      headerWnd = SendMessage_(hWnd,#LVM_GETHEADER,0,0)
      numCols = SendMessage_(headerWnd, #HDM_GETITEMCOUNT,0,0)
      Dim cols.l(numCols-1)
      SendMessage_(hWnd, #LVM_GETCOLUMNORDERARRAY, numCols, @cols())
      For i = 0 To numcols-1
        If cols(i)
          If cols(i) = *liedit\subItem
            Break
          EndIf
        Else
          blnFoundZeroColumn = 1      
        EndIf
      Next
      i-blnFoundZeroColumn 
      Dim cols(0)
      With hdi
        \mask = #HDI_ORDER 
        \iOrder = i
      EndWith      
      SendMessage_(headerWnd, #HDM_SETITEM, 0, hdi) 
    EndIf
  ;Scroll the listicon if the clicked cell is not entirely visible
  ;*****IF YOU WISH TO RESTRICT WHICH CELLS CAN BE EDITED, THEN PERFORM THE NECESSARY CHECKS HERE
  ;*****ON THE VALUES OF *liedit\item and *liedit\subitem (WHICH INDICATE WHICH CELL IS ABOUT
  ;*****TO BE EDITED) AND RUN THE FOLLOWING LINES FOR THOSE CELLS WHICH ARE TO BE EDITED.
  rc\top = *liedit\subitem
	rc\left = #LVIR_BOUNDS
	SendMessage_(hWnd, #LVM_GETSUBITEMRECT, *liedit\item, rc)
	GetClientRect_(hWnd, clientrc)
  If rc\left < 0 Or (rc\right-rc\left)>=clientrc\right
    SendMessage_(hWnd, #LVM_SCROLL,rc\left,0) 
  Else
	  If rc\right > clientrc\right
      SendMessage_(hWnd, #LVM_SCROLL,rc\right-clientrc\right,0) 
	  EndIf
	EndIf
  SetWindowLong_(hWnd, #GWL_STYLE, GetWindowLong_(hWnd, #GWL_STYLE)|#LVS_EDITLABELS)
  SendMessage_(hWnd, #LVM_EDITLABEL, *liedit\item, 0)
EndProcedure


;Window proc of the ListIcon parent window.
Procedure.i _LIEwinProc(hWnd, uMsg, wParam, lParam)
  Protected result, oldwinproc, *nmh. NMHDR, listhWnd, edithWnd, *liedit._LIEdit, *lvd.LV_DISPINFO, rc.RECT
  Protected hdi.HDITEM, headerWnd
  Static celltext$
  ;Retrieve the address of the old proc.
    oldwinproc = GetProp_(hWnd, "_LIEditOldProc")
  Select uMsg
    Case #WM_NOTIFY
      *nmh=lParam
      Select *nmh\code
        Case #LVN_BEGINLABELEDIT
          listhWnd = *nmh\hwndFrom
          ;Retrieve the address of the LIEdit structure.
            *liedit = GetProp_(listhWnd, "_LIEdit")
            If *liedit ;Good to go!
              *liedit\editHwnd=0
              ;Get the handle of the edit control used to edit the label.
                edithWnd = SendMessage_(listhWnd, #LVM_GETEDITCONTROL,0,0)
               ;Subclass the edit control.
                SetProp_(edithWnd, "_LIEditOldProc", SetWindowLong_(edithWnd, #GWL_WNDPROC, @_LIEeditProc()))
              ;Set text.
                celltext$=GetGadgetItemText(*nmh\idFrom, *liedit\item, *liedit\subitem)
                SendMessage_(edithWnd, #WM_SETTEXT, 0, celltext$)
                SetGadgetItemText(*nmh\idFrom, *liedit\item, "",*liedit\subitem)
               ;Get bounding rectangle.
                rc\top = *liedit\subitem
                rc\left = #LVIR_BOUNDS 
                SendMessage_(listhWnd, #LVM_GETSUBITEMRECT, *liedit\item, rc) 
                *liedit\x=rc\left
                *liedit\y=rc\top
                *liedit\cx=SendMessage_(listhWnd, #LVM_GETCOLUMNWIDTH, *liedit\subitem,0)
                *liedit\cy=rc\bottom-rc\top
            EndIf

        Case #LVN_ENDLABELEDIT
          listhWnd = *nmh\hwndFrom
          ;Retrieve the address of the LIEdit structure.
            *liedit = GetProp_(listhWnd, "_LIEdit")
            If *liedit ;Good to go!
Debug 2
              *lvd = lParam
              If *lvd\item\pszText
                SetGadgetItemText(*nmh\idFrom, *liedit\item, PeekS(*lvd\item\pszText), *liedit\subitem)
              Else              
                SetGadgetItemText(*nmh\idFrom, *liedit\item, celltext$, *liedit\subitem)
              EndIf
              SetWindowLong_(listhWnd, #GWL_STYLE, GetWindowLong_(listhWnd, #GWL_STYLE)&~#LVS_EDITLABELS)
              If _LIEditGlobals\osVersion >= #PB_OS_Windows_Vista And _LIEditGlobals\blnIsXPThemes 
                With hdi
                  \mask = #HDI_ORDER 
                  \iOrder = 0
                EndWith
                headerWnd = SendMessage_(listhWnd,#LVM_GETHEADER,0,0)
                SendMessage_(headerWnd, #HDM_SETITEM, 0, hdi) 
              EndIf
            EndIf
        Default
          result=CallWindowProc_(oldwinproc, hWnd, uMsg, wParam, lParam)
      EndSelect

    Case #WM_NCDESTROY
      result=CallWindowProc_(oldwinproc, hWnd, uMsg, wParam, lParam)
      RemoveProp_(hWnd, "_LIEditOldProc")
    Default
      result=CallWindowProc_(oldwinproc, hWnd, uMsg, wParam, lParam)
  EndSelect
  ProcedureReturn result
EndProcedure


;Window proc of the ListIcon.
Procedure.i _LIEListProc(hWnd, uMsg, wParam, lParam)
  Protected result, *liedit._LIEdit, PInfo.LVHITTESTINFO, *nmHEADER.HD_NOTIFY
  ;Retrieve the address of the LIEdit structure.
    *liedit = GetProp_(hWnd, "_LIEdit")
  Select uMsg
    Case #WM_NOTIFY
      *nmHEADER = lParam 
      Select *nmHEADER\hdr\code
        Case #HDN_BEGINTRACK, #HDN_BEGINTRACKW ;Prevent column 0 from being resized.
          If *nmHEADER\iItem=0
            result=1
          EndIf
        Case #HDN_ENDTRACK, #HDN_ENDTRACKW
          InvalidateRect_(hWnd,0,1)
        Default
          result=CallWindowProc_(*liedit\listOldProc, hWnd, uMsg, wParam, lParam)
      EndSelect
    Case #WM_LBUTTONDBLCLK
      ;Identify the clicked item
        PInfo\pt\x = lParam&$ffff 
        PInfo\pt\y = (lParam>>16)&$ffff 
        SendMessage_(hwnd, #LVM_SUBITEMHITTEST, 0, PInfo) 
      If PInfo\iItem <> -1 ;A valid cell was clicked.
        *liedit\item = PInfo\iItem
        *liedit\subitem = PInfo\iSubItem
        _LIEEditCell(*liedit, hWnd)
      EndIf
    Case #WM_NCDESTROY
      result=CallWindowProc_(*liedit\listOldProc, hWnd, uMsg, wParam, lParam)
      RemoveProp_(hWnd, "_LIEdit")
      FreeMemory(*liedit)
    Default
      result=CallWindowProc_(*liedit\listOldProc, hWnd, uMsg, wParam, lParam)
  EndSelect
  ProcedureReturn result
EndProcedure


;Window proc of the edit control.
Procedure.i _LIEeditProc(hWnd, uMsg, wParam, lParam)
  Protected result, oldwinproc, *liedit._LIEdit, *wpos.WINDOWPOS
  ;Retrieve the address of the old proc.
    oldwinproc = GetProp_(hWnd, "_LIEditOldProc")
  ;Retrieve the address of the LIEdit structure.
    *liedit = GetProp_(GetParent_(hWnd), "_LIEdit")
  Select uMsg
    Case #WM_ERASEBKGND
      ;A hack in order to clear the default selection of characters.
      result=CallWindowProc_(oldwinproc, hWnd, uMsg, wParam, lParam)
      If *liedit\editHwnd=0
        *liedit\editHwnd = hWnd
        ;Set margins.
        SendMessage_(hWnd, #EM_SETMARGINS, #EC_LEFTMARGIN|#EC_RIGHTMARGIN, 4)
        SendMessage_(hWnd, #EM_SETSEL, -1,0)
      EndIf
    Case #WM_WINDOWPOSCHANGING
      *wpos=lParam
      *wpos\cx=*liedit\cx ;Comment this line to get an edit control which grows with the text.
      *wpos\x=*liedit\x
      If _LIEditGlobals\osVersion >= #PB_OS_Windows_Vista And _LIEditGlobals\blnIsXPThemes 
        *wpos\cy=*liedit\cy
        *wpos\y=*liedit\y
      Else
        *wpos\cy=*liedit\cy+3
        *wpos\y=*liedit\y-2
      EndIf
      result=CallWindowProc_(oldwinproc, hWnd, uMsg, wParam, lParam)
    Case #WM_NCDESTROY
      result=CallWindowProc_(oldwinproc, hWnd, uMsg, wParam, lParam)
      RemoveProp_(hWnd, "_LIEditOldProc")
      ;InvalidateRect_(GetParent_(hWnd),0,0)
    Default
      result=CallWindowProc_(oldwinproc, hWnd, uMsg, wParam, lParam)
  EndSelect
  ProcedureReturn result
EndProcedure

NOTE that for this to work, you must blank out column zero by setting it's width to zero. The library automatically prevents the user from resizing this column.

Posted: Fri May 11, 2007 2:33 am
by srod
Example:

Code: Select all

XIncludeFile "Edit ListIcon.pbi"
DisableExplicit

LoadFont(1, "Arial",10)

If OpenWindow(0, 100, 100, 600, 600, "ListIcon Example", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
  If CreateGadgetList(WindowID(0))
    ListIconGadget(1, 5, 5, 490, 390, "", 0, #PB_ListIcon_GridLines)
    SetGadgetFont(1, FontID(1))
    For i = 1 To 30
      AddGadgetColumn(1, i, "Col "+Str(i), 100)
    Next
    For row = 1 To 100
      AddGadgetItem(1,-1,"")
      For col = 2 To 10
        SetGadgetItemText(1, row-1, "row "+Str(row-1)+", col "+Str(col), col)
      Next
    Next

    SetListIconEditable(1)
;    EditCell(1, 19, 5)
    Repeat
      Event = WaitWindowEvent()
    Until Event = #PB_Event_CloseWindow
  EndIf
EndIf

Posted: Fri May 11, 2007 2:57 am
by Hroudtwolf
Very nice.
Thanks for sharing. :)

A very orderly source.

Posted: Fri May 11, 2007 5:36 am
by Dare
Nifty and learn-from-able.

Thanks.

Posted: Fri May 11, 2007 8:33 am
by srod
Hroudtwolf wrote:A very orderly source.
Now there's a title for a book! :)

Posted: Fri May 11, 2007 9:33 am
by srod
Update: 11/05/07.
Added the command EditCell() to allow a cell to be edited from code.

See first post.

Posted: Fri May 11, 2007 10:22 am
by mueckerich
Thanks for sharing, this one will help me a lot in my new project. :D

Posted: Fri May 11, 2007 10:27 am
by milan1612
Hey srod,
how to implement cell editing for a specific column only?
(Would be a nice feature for a "property grid")
thanks...

Posted: Fri May 11, 2007 12:44 pm
by srod
milan1612 wrote:Hey srod,
how to implement cell editing for a specific column only?
(Would be a nice feature for a "property grid")
thanks...
For that, you'll have to roll your sleeves up and adjust the source! :)

An easy adjustment to make.

I've edited the source in my first post to show you how to adjust the code to restrict which cells can be edited etc. You only need to look in the _LIEListProc() procedure and add a couple of checks on the row and column indexes of the cell to be edited etc.

Posted: Fri May 11, 2007 12:50 pm
by milan1612
OK thanks, I'll take a look. I was just too lazy... :lol:

Posted: Fri May 11, 2007 12:56 pm
by srod
milan1612 wrote:OK thanks, I'll take a look. I was just too lazy... :lol:
:)

Posted: Fri May 11, 2007 12:59 pm
by rsts
Very impressive code. I believe there's lots to learn from this.

Many thanks for sharing.

cheers

Posted: Fri May 11, 2007 1:05 pm
by srod
You're welcome. 8)

As I say I was just wondering if editing cells in this manner was possible. Of course, the method is equivalent to superimposing an edit control over the cell to be edited ourselves etc, but windows, through the #LVS_EDITLABELS style, takes some of the load off! It does mean, however, that this method is not as flexible as superimposing an edit control ourselves, but it should suffice for many applications.

Posted: Fri May 11, 2007 6:19 pm
by Xombie
I like how you say just "tinkering" and then pump out a large chunk of complex code :) Nice job.

One thing I noticed - Fred snuck in GadgetType() at some point and I had no idea. That sneaky...

Also, not sure why my name was mentioned? I don't think I ever had a method of editing listicon cells.

Posted: Sat May 12, 2007 11:09 am
by srod
Xombie wrote:I like how you say just "tinkering" and then pump out a large chunk of complex code :) Nice job.
Wait until you see what happens when I claim to be working on a complex piece of code - I churn out a piece of s*it! :wink:
One thing I noticed - Fred snuck in GadgetType() at some point and I had no idea. That sneaky...
Aye, in the past I've used GetClassName_() etc, but this way is nice.
Also, not sure why my name was mentioned? I don't think I ever had a method of editing listicon cells.
Consider yourself removed from the editable ListIcon hall of fame and dumped into the hall of shame! :D

Actually, I think that xgrid warrants a sepcial invite into the hall of fame. :wink: