322 lines
11 KiB
Python
322 lines
11 KiB
Python
|
# hierlist
|
||
|
#
|
||
|
# IMPORTANT - Please read before using.
|
||
|
|
||
|
# This module exposes an API for a Hierarchical Tree Control.
|
||
|
# Previously, a custom tree control was included in Pythonwin which
|
||
|
# has an API very similar to this.
|
||
|
|
||
|
# The current control used is the common "Tree Control". This module exists now
|
||
|
# to provide an API similar to the old control, but for the new Tree control.
|
||
|
|
||
|
# If you need to use the Tree Control, you may still find this API a reasonable
|
||
|
# choice. However, you should investigate using the tree control directly
|
||
|
# to provide maximum flexibility (but with extra work).
|
||
|
|
||
|
import sys
|
||
|
import win32ui
|
||
|
import win32con
|
||
|
import win32api
|
||
|
from win32api import RGB
|
||
|
|
||
|
from pywin.mfc import object, window, docview, dialog
|
||
|
import commctrl
|
||
|
|
||
|
# helper to get the text of an arbitary item
|
||
|
def GetItemText(item):
|
||
|
if type(item)==type(()) or type(item)==type([]):
|
||
|
use = item[0]
|
||
|
else:
|
||
|
use = item
|
||
|
if type(use)==type(''):
|
||
|
return use
|
||
|
else:
|
||
|
return repr(item)
|
||
|
|
||
|
|
||
|
class HierDialog(dialog.Dialog):
|
||
|
def __init__(self, title, hierList, bitmapID = win32ui.IDB_HIERFOLDERS, dlgID = win32ui.IDD_TREE, dll = None, childListBoxID = win32ui.IDC_LIST1):
|
||
|
dialog.Dialog.__init__(self, dlgID, dll ) # reuse this dialog.
|
||
|
self.hierList=hierList
|
||
|
self.dlgID = dlgID
|
||
|
self.title=title
|
||
|
# self.childListBoxID = childListBoxID
|
||
|
def OnInitDialog(self):
|
||
|
self.SetWindowText(self.title)
|
||
|
self.hierList.HierInit(self)
|
||
|
return dialog.Dialog.OnInitDialog(self)
|
||
|
|
||
|
class HierList(object.Object):
|
||
|
def __init__(self, root, bitmapID = win32ui.IDB_HIERFOLDERS, listBoxId = None, bitmapMask = None): # used to create object.
|
||
|
self.listControl = None
|
||
|
self.bitmapID = bitmapID
|
||
|
self.root = root
|
||
|
self.listBoxId = listBoxId
|
||
|
self.itemHandleMap = {}
|
||
|
self.filledItemHandlesMap = {}
|
||
|
self.bitmapMask = bitmapMask
|
||
|
def __getattr__(self, attr):
|
||
|
try:
|
||
|
return getattr(self.listControl, attr)
|
||
|
except AttributeError:
|
||
|
return object.Object.__getattr__(self, attr)
|
||
|
|
||
|
def ItemFromHandle(self, handle):
|
||
|
return self.itemHandleMap[handle]
|
||
|
def SetStyle(self, newStyle):
|
||
|
hwnd = self.listControl.GetSafeHwnd()
|
||
|
style = win32api.GetWindowLong(hwnd, win32con.GWL_STYLE);
|
||
|
win32api.SetWindowLong(hwnd, win32con.GWL_STYLE, (style | newStyle) )
|
||
|
|
||
|
def HierInit(self, parent, listControl = None ): # Used when window first exists.
|
||
|
# this also calls "Create" on the listbox.
|
||
|
# params - id of listbbox, ID of bitmap, size of bitmaps
|
||
|
if self.bitmapMask is None:
|
||
|
bitmapMask = RGB(0,0,255)
|
||
|
else:
|
||
|
bitmapMask = self.bitmapMask
|
||
|
self.imageList = win32ui.CreateImageList(self.bitmapID, 16, 0, bitmapMask)
|
||
|
if listControl is None:
|
||
|
if self.listBoxId is None: self.listBoxId = win32ui.IDC_LIST1
|
||
|
self.listControl = parent.GetDlgItem(self.listBoxId)
|
||
|
else:
|
||
|
self.listControl = listControl
|
||
|
lbid = listControl.GetDlgCtrlID()
|
||
|
assert self.listBoxId is None or self.listBoxId == lbid, "An invalid listbox control ID has been specified (specified as %s, but exists as %s)" % (self.listBoxId, lbid)
|
||
|
self.listBoxId = lbid
|
||
|
self.listControl.SetImageList(self.imageList, commctrl.LVSIL_NORMAL)
|
||
|
# self.list.AttachObject(self)
|
||
|
|
||
|
## ??? Need a better way to do this - either some way to detect if it's compiled with UNICODE
|
||
|
## defined, and/or a way to switch the constants based on UNICODE ???
|
||
|
if sys.version_info[0] < 3:
|
||
|
parent.HookNotify(self.OnTreeItemExpanding, commctrl.TVN_ITEMEXPANDINGA)
|
||
|
parent.HookNotify(self.OnTreeItemSelChanged, commctrl.TVN_SELCHANGEDA)
|
||
|
else:
|
||
|
parent.HookNotify(self.OnTreeItemExpanding, commctrl.TVN_ITEMEXPANDINGW)
|
||
|
parent.HookNotify(self.OnTreeItemSelChanged, commctrl.TVN_SELCHANGEDW)
|
||
|
parent.HookNotify(self.OnTreeItemDoubleClick, commctrl.NM_DBLCLK)
|
||
|
self.notify_parent = parent
|
||
|
|
||
|
if self.root:
|
||
|
self.AcceptRoot(self.root)
|
||
|
|
||
|
def DeleteAllItems(self):
|
||
|
self.listControl.DeleteAllItems()
|
||
|
self.root = None
|
||
|
self.itemHandleMap = {}
|
||
|
self.filledItemHandlesMap = {}
|
||
|
|
||
|
def HierTerm(self):
|
||
|
# Dont want notifies as we kill the list.
|
||
|
parent = self.notify_parent # GetParentFrame()
|
||
|
if sys.version_info[0] < 3:
|
||
|
parent.HookNotify(None, commctrl.TVN_ITEMEXPANDINGA)
|
||
|
parent.HookNotify(None, commctrl.TVN_SELCHANGEDA)
|
||
|
else:
|
||
|
parent.HookNotify(None, commctrl.TVN_ITEMEXPANDINGW)
|
||
|
parent.HookNotify(None, commctrl.TVN_SELCHANGEDW)
|
||
|
parent.HookNotify(None, commctrl.NM_DBLCLK)
|
||
|
|
||
|
self.DeleteAllItems()
|
||
|
self.listControl = None
|
||
|
self.notify_parent = None # Break a possible cycle
|
||
|
|
||
|
def OnTreeItemDoubleClick(self, info, extra):
|
||
|
(hwndFrom, idFrom, code) = info
|
||
|
if idFrom != self.listBoxId: return None
|
||
|
item = self.itemHandleMap[self.listControl.GetSelectedItem()]
|
||
|
self.TakeDefaultAction(item)
|
||
|
return 1
|
||
|
|
||
|
def OnTreeItemExpanding(self, info, extra):
|
||
|
(hwndFrom, idFrom, code) = info
|
||
|
if idFrom != self.listBoxId: return None
|
||
|
action, itemOld, itemNew, pt = extra
|
||
|
itemHandle = itemNew[0]
|
||
|
if itemHandle not in self.filledItemHandlesMap:
|
||
|
item = self.itemHandleMap[itemHandle]
|
||
|
self.AddSubList(itemHandle, self.GetSubList(item))
|
||
|
self.filledItemHandlesMap[itemHandle] = None
|
||
|
return 0
|
||
|
|
||
|
def OnTreeItemSelChanged(self, info, extra):
|
||
|
(hwndFrom, idFrom, code) = info
|
||
|
if idFrom != self.listBoxId: return None
|
||
|
action, itemOld, itemNew, pt = extra
|
||
|
itemHandle = itemNew[0]
|
||
|
item = self.itemHandleMap[itemHandle]
|
||
|
self.PerformItemSelected(item)
|
||
|
return 1
|
||
|
|
||
|
def AddSubList(self, parentHandle, subItems):
|
||
|
for item in subItems:
|
||
|
self.AddItem(parentHandle, item)
|
||
|
|
||
|
def AddItem(self, parentHandle, item, hInsertAfter = commctrl.TVI_LAST):
|
||
|
text = self.GetText(item)
|
||
|
if self.IsExpandable(item):
|
||
|
cItems = 1 # Trick it !!
|
||
|
else:
|
||
|
cItems = 0
|
||
|
bitmapCol = self.GetBitmapColumn(item)
|
||
|
bitmapSel = self.GetSelectedBitmapColumn(item)
|
||
|
if bitmapSel is None: bitmapSel = bitmapCol
|
||
|
## if type(text) is str:
|
||
|
## text = text.encode("mbcs")
|
||
|
hitem = self.listControl.InsertItem(parentHandle, hInsertAfter, (None, None, None, text, bitmapCol, bitmapSel, cItems, 0))
|
||
|
self.itemHandleMap[hitem] = item
|
||
|
return hitem
|
||
|
|
||
|
def _GetChildHandles(self, handle):
|
||
|
ret = []
|
||
|
try:
|
||
|
handle = self.listControl.GetChildItem(handle)
|
||
|
while 1:
|
||
|
ret.append(handle)
|
||
|
handle = self.listControl.GetNextItem(handle, commctrl.TVGN_NEXT)
|
||
|
except win32ui.error:
|
||
|
# out of children
|
||
|
pass
|
||
|
return ret
|
||
|
def ItemFromHandle(self, handle):
|
||
|
return self.itemHandleMap[handle]
|
||
|
|
||
|
def Refresh(self, hparent = None):
|
||
|
# Attempt to refresh the given item's sub-entries, but maintain the tree state
|
||
|
# (ie, the selected item, expanded items, etc)
|
||
|
if hparent is None: hparent = commctrl.TVI_ROOT
|
||
|
if hparent not in self.filledItemHandlesMap:
|
||
|
# This item has never been expanded, so no refresh can possibly be required.
|
||
|
return
|
||
|
root_item = self.itemHandleMap[hparent]
|
||
|
old_handles = self._GetChildHandles(hparent)
|
||
|
old_items = list(map( self.ItemFromHandle, old_handles ))
|
||
|
new_items = self.GetSubList(root_item)
|
||
|
# Now an inefficient technique for synching the items.
|
||
|
inew = 0
|
||
|
hAfter = commctrl.TVI_FIRST
|
||
|
for iold in range(len(old_items)):
|
||
|
inewlook = inew
|
||
|
matched = 0
|
||
|
while inewlook < len(new_items):
|
||
|
if old_items[iold] == new_items[inewlook]:
|
||
|
matched = 1
|
||
|
break
|
||
|
inewlook = inewlook + 1
|
||
|
if matched:
|
||
|
# Insert the new items.
|
||
|
# print "Inserting after", old_items[iold], old_handles[iold]
|
||
|
for i in range(inew, inewlook):
|
||
|
# print "Inserting index %d (%s)" % (i, new_items[i])
|
||
|
hAfter = self.AddItem(hparent, new_items[i], hAfter)
|
||
|
|
||
|
inew = inewlook + 1
|
||
|
# And recursively refresh iold
|
||
|
hold = old_handles[iold]
|
||
|
if hold in self.filledItemHandlesMap:
|
||
|
self.Refresh(hold)
|
||
|
else:
|
||
|
# Remove the deleted items.
|
||
|
# print "Deleting %d (%s)" % (iold, old_items[iold])
|
||
|
hdelete = old_handles[iold]
|
||
|
# First recurse and remove the children from the map.
|
||
|
for hchild in self._GetChildHandles(hdelete):
|
||
|
del self.itemHandleMap[hchild]
|
||
|
if hchild in self.filledItemHandlesMap:
|
||
|
del self.filledItemHandlesMap[hchild]
|
||
|
self.listControl.DeleteItem(hdelete)
|
||
|
hAfter = old_handles[iold]
|
||
|
# Fill any remaining new items:
|
||
|
for newItem in new_items[inew:]:
|
||
|
# print "Inserting new item", newItem
|
||
|
self.AddItem(hparent, newItem)
|
||
|
def AcceptRoot(self, root):
|
||
|
self.listControl.DeleteAllItems()
|
||
|
self.itemHandleMap = {commctrl.TVI_ROOT : root}
|
||
|
self.filledItemHandlesMap = {commctrl.TVI_ROOT : root}
|
||
|
subItems = self.GetSubList(root)
|
||
|
self.AddSubList(0, subItems)
|
||
|
|
||
|
def GetBitmapColumn(self, item):
|
||
|
if self.IsExpandable(item):
|
||
|
return 0
|
||
|
else:
|
||
|
return 4
|
||
|
def GetSelectedBitmapColumn(self, item):
|
||
|
return None # Use standard.
|
||
|
|
||
|
def GetSelectedBitmapColumn(self, item):
|
||
|
return 0
|
||
|
|
||
|
def CheckChangedChildren(self):
|
||
|
return self.listControl.CheckChangedChildren()
|
||
|
def GetText(self,item):
|
||
|
return GetItemText(item)
|
||
|
def PerformItemSelected(self, item):
|
||
|
try:
|
||
|
win32ui.SetStatusText('Selected ' + self.GetText(item))
|
||
|
except win32ui.error: # No status bar!
|
||
|
pass
|
||
|
def TakeDefaultAction(self, item):
|
||
|
win32ui.MessageBox('Got item ' + self.GetText(item))
|
||
|
|
||
|
##########################################################################
|
||
|
#
|
||
|
# Classes for use with seperate HierListItems.
|
||
|
#
|
||
|
#
|
||
|
class HierListWithItems(HierList):
|
||
|
def __init__(self, root, bitmapID = win32ui.IDB_HIERFOLDERS, listBoxID = None, bitmapMask = None): # used to create object.
|
||
|
HierList.__init__(self, root, bitmapID, listBoxID, bitmapMask )
|
||
|
def DelegateCall( self, fn):
|
||
|
return fn()
|
||
|
def GetBitmapColumn(self, item):
|
||
|
rc = self.DelegateCall(item.GetBitmapColumn)
|
||
|
if rc is None:
|
||
|
rc = HierList.GetBitmapColumn(self, item)
|
||
|
return rc
|
||
|
def GetSelectedBitmapColumn(self, item):
|
||
|
return self.DelegateCall(item.GetSelectedBitmapColumn)
|
||
|
def IsExpandable(self, item):
|
||
|
return self.DelegateCall( item.IsExpandable)
|
||
|
def GetText(self, item):
|
||
|
return self.DelegateCall( item.GetText )
|
||
|
def GetSubList(self, item):
|
||
|
return self.DelegateCall(item.GetSubList)
|
||
|
def PerformItemSelected(self, item):
|
||
|
func = getattr(item, "PerformItemSelected", None)
|
||
|
if func is None:
|
||
|
return HierList.PerformItemSelected( self, item )
|
||
|
else:
|
||
|
return self.DelegateCall(func)
|
||
|
|
||
|
def TakeDefaultAction(self, item):
|
||
|
func = getattr(item, "TakeDefaultAction", None)
|
||
|
if func is None:
|
||
|
return HierList.TakeDefaultAction( self, item )
|
||
|
else:
|
||
|
return self.DelegateCall(func)
|
||
|
|
||
|
# A hier list item - for use with a HierListWithItems
|
||
|
class HierListItem:
|
||
|
def __init__(self):
|
||
|
pass
|
||
|
def GetText(self):
|
||
|
pass
|
||
|
def GetSubList(self):
|
||
|
pass
|
||
|
def IsExpandable(self):
|
||
|
pass
|
||
|
def GetBitmapColumn(self):
|
||
|
return None # indicate he should do it.
|
||
|
def GetSelectedBitmapColumn(self):
|
||
|
return None # same as other
|
||
|
# for py3k/rich-comp sorting compatibility.
|
||
|
def __lt__(self, other):
|
||
|
# we want unrelated items to be sortable...
|
||
|
return id(self) < id(other)
|
||
|
# for py3k/rich-comp equality compatibility.
|
||
|
def __eq__(self, other):
|
||
|
return False
|