📄 listctrl.py
字号:
# The contents of this file are subject to the BitTorrent Open Source License# Version 1.1 (the License). You may not copy or use this file, in either# source code or executable form, except in compliance with the License. You# may obtain a copy of the License at http://www.bittorrent.com/license/.## Software distributed under the License is distributed on an AS IS basis,# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License# for the specific language governing rights and limitations under the# License.# written by Matt Chisholm, Steven Hazel, and Greg Hazelimport localeimport tracebackimport wxfrom UserDict import IterableUserDictfrom BitTorrent.obsoletepythonsupport import setfrom wx.lib.mixins.listctrl import ColumnSorterMixinfrom wx.lib.mixins.listctrl import getListCtrlSelectionimport osif os.name == 'nt': LVM_FIRST = 0x1000 LVM_SETSELECTEDCOLUMN = (LVM_FIRST + 140) import win32apidef highlight_color(c): if c > 240: c *= 0.97 else: c = min(c * 1.10, 255) return int(c)SEL_FOC = wx.LIST_STATE_SELECTED | wx.LIST_STATE_FOCUSEDdef selectBeforePopup(ctrl, pos): """Ensures the item the mouse is pointing at is selected before a popup. Works with both single-select and multi-select lists.""" if not isinstance(ctrl, wx.ListCtrl): return n, flags = ctrl.HitTest(pos) if n < 0: return if not ctrl.GetItemState(n, wx.LIST_STATE_SELECTED): for i in xrange(ctrl.GetItemCount()): ctrl.SetItemState(i, 0, SEL_FOC) ctrl.SetItemState(n, SEL_FOC, SEL_FOC)class ContextMenuMixin(object): def __init__(self): self.context_menu = None self.column_context_menu = None self.Bind(wx.EVT_CONTEXT_MENU, self.OnContextMenu) self.Bind(wx.EVT_LIST_COL_RIGHT_CLICK, self.OnColumnContextMenu) def SetContextMenu(self, menu): self.context_menu = menu def SetColumnContextMenu(self, menu): self.column_context_menu = menu def OnColumnContextMenu(self, event): if self.column_context_menu: self.PopupMenu(self.column_context_menu) def OnContextMenu(self, event): pos = self.ScreenToClient(event.GetPosition()) top = self.GetItemRect(self.GetTopItem()) if pos[1] < top.y: event.Skip() return pos -= self._get_origin_offset() self.DoPopup(pos) def DoPopup(self, pos): """ pos should be in client coords """ if self.context_menu: selectBeforePopup(self, pos) selection = getListCtrlSelection(self) if len(selection) > 0: self.PopupMenu(self.context_menu) returnclass BTListColumn(wx.ListItem): def __init__(self, text, sample_data, renderer=None, comparator=None, enabled=True, width=50): wx.ListItem.__init__(self) self.SetText(text) self.renderer = renderer self.comparator = comparator self.enabled = enabled self.sample_data = sample_data self.width = widthclass BTListRow(IterableUserDict): __slots__ = ['data', 'index'] def __init__(self, index, data): self.data = data self.index = index def __getitem__(self, i): return self.data[i]class BTListCtrl(wx.ListCtrl, ColumnSorterMixin, ContextMenuMixin): # Part of this class based on: # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/426407 icon_size = 16 def __init__(self, parent): wx.ListCtrl.__init__(self, parent, wx.ID_ANY, style=wx.LC_REPORT) ContextMenuMixin.__init__(self) self.il = wx.ImageList(self.icon_size, self.icon_size) # TODO: use a real icon self.il.Add(self.draw_blank()) self.il.Add(self.draw_sort_arrow('up')) self.il.Add(self.draw_sort_arrow('down')) self.SetImageList(self.il, wx.IMAGE_LIST_SMALL) self.update_enabled_columns() for i, name in enumerate(self.enabled_columns): column = self.columns[name] column.SetColumn(i) self.InsertColumnItem(i, column) self.itemData_to_row = {} self.index_to_itemData = {} self.selected_column = None self.SelectColumn(self.enabled_columns[0]) cmenu = wx.Menu() for name in self.column_order: column = self.columns[name] id = wx.NewId() cmenu.AppendCheckItem(id, column.GetText()) cmenu.Check(id, column.enabled) self.Bind(wx.EVT_MENU, lambda e, c=column, id=id: self.toggle_column(c, id, e), id=id) self.SetColumnContextMenu(cmenu) ColumnSorterMixin.__init__(self, len(self.enabled_columns)) self._last_scrollpos = 0 self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground) self.default_rect = wx.Rect(0,0) def OnEraseBackground(self, event=None): nsp = self.GetScrollPos(wx.VERTICAL) if self._last_scrollpos != nsp: self._last_scrollpos = nsp # should only refresh visible items, hmm wx.CallAfter(self.Refresh) dc = wx.ClientDC(self) # erase the section of the background which is not covered by the # items or the selected column highlighting dc.SetBackground(wx.Brush(self.GetBackgroundColour())) f = self.GetRect() r = wx.Region(0, 0, f.width, f.height) x = self.GetVisibleViewRect() offset = self._get_origin_offset(include_header=True) x.Offset(offset) r.SubtractRect(x) if '__WXMSW__' in wx.PlatformInfo: c = self.GetColumnRect(self.enabled_columns.index(self.selected_column)) r.SubtractRect(c) dc.SetClippingRegionAsRegion(r) dc.Clear() if '__WXMSW__' in wx.PlatformInfo: # draw the selected column highlighting under the items dc.DestroyClippingRegion() r = wx.Region(0, 0, f.width, f.height) r.SubtractRect(x) dc.SetClippingRegionAsRegion(r) dc.SetPen(wx.TRANSPARENT_PEN) hc = wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW) r = highlight_color(hc.Red()) g = highlight_color(hc.Green()) b = highlight_color(hc.Blue()) hc.Set(r, g, b) dc.SetBrush(wx.Brush(hc)) dc.DrawRectangle(c.x, c.y, c.width, c.height) def update_enabled_columns(self): self.enabled_columns = [name for name in self.column_order if self.columns[name].enabled] def toggle_column(self, tcolumn, id, event): self.update_column_widths() sort_col = self.get_sort_column() tcolumn.enabled = not tcolumn.enabled self.column_context_menu.Check(id, tcolumn.enabled) self.update_enabled_columns() if not tcolumn.enabled: self.DeleteColumn(tcolumn.GetColumn()) new_col_names = [] for i, name in enumerate(self.enabled_columns): column = self.columns[name] column.SetColumn(i) if column == tcolumn: self.InsertColumnItem(i, column) new_col_names.append(name) self.SetColumnWidth(column.GetColumn(), column.width) self.SetColumnCount(len(self.enabled_columns)) self.SortListItems(col=sort_col) for itemData in self.itemData_to_row.iterkeys(): self.InsertRow(itemData, self.itemData_to_row[itemData], sort=True, force_update_columns=new_col_names) #self.SortItems() def set_default_widths(self): # must be called before *any* data is put into the control. sample_data = {} for name in self.column_order: sample_data[name] = self.columns[name].sample_data sample_row = BTListRow(None, sample_data) self.InsertRow(-1, sample_row) for name in self.column_order: column = self.columns[name] if name in self.enabled_columns: self.SetColumnWidth(column.GetColumn(), wx.LIST_AUTOSIZE) column.width = self.GetColumnWidth(column.GetColumn()) dc = wx.ClientDC(self) header_width = dc.GetTextExtent(column.GetText())[0] header_width += 4 # arbitrary allowance for header decorations column.width = max(column.width, header_width) if name in self.enabled_columns: self.SetColumnWidth(column.GetColumn(), column.width) self.default_rect = self.GetItemRect(0) self.DeleteRow(-1) def _get_origin_offset(self, include_header=None): if include_header is None: # Hm, I think this is a legit bug in wxGTK if '__WXGTK__' in wx.PlatformInfo: include_header = True else: include_header = False if include_header: i = self.GetTopItem() try: r = self.GetItemRect(i) except wx._core.PyAssertionError: r = self.default_rect return (r.x, r.y) return (0, 0) def add_image(self, image): b = wx.BitmapFromImage(image) assert b.Ok(), "The image (%s) is not valid." % str(image) if '__WXMSW__' in wx.PlatformInfo: # hack for 16-bit color mode if b.GetDepth() > 24: # I mean, has alpha b.SetMask(wx.Mask(b, wx.Colour(0, 0, 0))) return self.il.Add(b) # Arrow drawing def draw_blank(self): b = wx.EmptyBitmap(self.icon_size, self.icon_size) dc = wx.MemoryDC() dc.SelectObject(b) dc.SetBackgroundMode(wx.TRANSPARENT) dc.Clear() dc.SelectObject(wx.NullBitmap) b.SetMask(wx.Mask(b, (255, 255, 255))) return b # this builds an identical arrow to the windows listctrl arrows, in themed # and non-themed mode. def draw_sort_arrow(self, direction): b = wx.EmptyBitmap(self.icon_size, self.icon_size) w, h = b.GetSize() ho = (h - 5) / 2 dc = wx.MemoryDC() dc.SelectObject(b) colour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_GRAYTEXT) dc.SetBackgroundMode(wx.TRANSPARENT) dc.Clear() dc.SetPen(wx.Pen(colour)) for i in xrange(5): if direction == 'down': j = 4 - i else: j = i dc.DrawLine(i,j+ho,9-i,j+ho) dc.SelectObject(wx.NullBitmap) b.SetMask(wx.Mask(b, (255, 255, 255))) return b def GetBottomItem(self): total = self.GetItemCount() top = self.GetTopItem() pp = self.GetCountPerPage() # I purposefully do not subtract 1 from pp, because pp is whole items bottom = min(top + pp, total - 1) return bottom def SelectColumn(self, col): """Color the selected column (MSW only)""" if self.selected_column == col: return col_num = self.enabled_columns.index(col) if os.name == 'nt': win32api.PostMessage(self.GetHandle(), LVM_SETSELECTEDCOLUMN, col_num, 0) if self.selected_column is not None: self.RefreshCol(self.selected_column) self.RefreshCol(col) self.selected_column = col def render_column_text(self, row, name): """Renders the column value into a string""" item = self.columns[name] value = row[name] if value is None: text = '?' elif item.renderer is not None: try: text = item.renderer(value)
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -