How to list only the visible items in QTableView in pyqt - pyqt4

I have below code to get filters for QTableView. But i am not able to filter multiple column at a time.
i.e., if filter column 2 with row 0 col 0 and try to filter column 2, it should show only the visible unique values of column 2 (probably it should show row 0 col 1 only) but now its showing all the elements of column 2 (row 0 col 1, row 1 col 1, row 2 col 1)
#-*- coding:utf-8 -*-
from PyQt4 import QtCore, QtGui
class myWindow(QtGui.QMainWindow):
def __init__(self, parent=None):
super(myWindow, self).__init__(parent)
self.centralwidget = QtGui.QWidget(self)
self.lineEdit = QtGui.QLineEdit(self.centralwidget)
self.view = QtGui.QTableView(self.centralwidget)
self.comboBox = QtGui.QComboBox(self.centralwidget)
self.label = QtGui.QLabel(self.centralwidget)
self.gridLayout = QtGui.QGridLayout(self.centralwidget)
self.gridLayout.addWidget(self.lineEdit, 0, 1, 1, 1)
self.gridLayout.addWidget(self.view, 1, 0, 1, 3)
self.gridLayout.addWidget(self.comboBox, 0, 2, 1, 1)
self.gridLayout.addWidget(self.label, 0, 0, 1, 1)
self.setCentralWidget(self.centralwidget)
self.label.setText("Regex Filter")
self.model = QtGui.QStandardItemModel(self)
for rowName in range(3) * 5:
self.model.invisibleRootItem().appendRow(
[ QtGui.QStandardItem("row {0} col {1}".format(rowName, column))
for column in range(3)
]
)
self.proxy = QtGui.QSortFilterProxyModel(self)
self.proxy.setSourceModel(self.model)
self.view.setModel(self.proxy)
self.comboBox.addItems(["Column {0}".format(x) for x in range(self.model.columnCount())])
self.lineEdit.textChanged.connect(self.on_lineEdit_textChanged)
self.comboBox.currentIndexChanged.connect(self.on_comboBox_currentIndexChanged)
self.horizontalHeader = self.view.horizontalHeader()
self.horizontalHeader.sectionClicked.connect(self.on_view_horizontalHeader_sectionClicked)
#QtCore.pyqtSlot(int)
def on_view_horizontalHeader_sectionClicked(self, logicalIndex):
self.logicalIndex = logicalIndex
self.menuValues = QtGui.QMenu(self)
self.signalMapper = QtCore.QSignalMapper(self)
self.comboBox.blockSignals(True)
self.comboBox.setCurrentIndex(self.logicalIndex)
self.comboBox.blockSignals(True)
valuesUnique = [ self.model.item(row, self.logicalIndex).text()
for row in range(self.model.rowCount())
]
actionAll = QtGui.QAction("All", self)
actionAll.triggered.connect(self.on_actionAll_triggered)
self.menuValues.addAction(actionAll)
self.menuValues.addSeparator()
for actionNumber, actionName in enumerate(sorted(list(set(valuesUnique)))):
action = QtGui.QAction(actionName, self)
self.signalMapper.setMapping(action, actionNumber)
action.triggered.connect(self.signalMapper.map)
self.menuValues.addAction(action)
self.signalMapper.mapped.connect(self.on_signalMapper_mapped)
headerPos = self.view.mapToGlobal(self.horizontalHeader.pos())
posY = headerPos.y() + self.horizontalHeader.height()
posX = headerPos.x() + self.horizontalHeader.sectionPosition(self.logicalIndex)
self.menuValues.exec_(QtCore.QPoint(posX, posY))
#QtCore.pyqtSlot()
def on_actionAll_triggered(self):
filterColumn = self.logicalIndex
filterString = QtCore.QRegExp( "",
QtCore.Qt.CaseInsensitive,
QtCore.QRegExp.RegExp
)
self.proxy.setFilterRegExp(filterString)
self.proxy.setFilterKeyColumn(filterColumn)
#QtCore.pyqtSlot(int)
def on_signalMapper_mapped(self, i):
stringAction = self.signalMapper.mapping(i).text()
filterColumn = self.logicalIndex
filterString = QtCore.QRegExp( stringAction,
QtCore.Qt.CaseSensitive,
QtCore.QRegExp.FixedString
)
self.proxy.setFilterRegExp(filterString)
self.proxy.setFilterKeyColumn(filterColumn)
#QtCore.pyqtSlot(str)
def on_lineEdit_textChanged(self, text):
search = QtCore.QRegExp( text,
QtCore.Qt.CaseInsensitive,
QtCore.QRegExp.RegExp
)
self.proxy.setFilterRegExp(search)
#QtCore.pyqtSlot(int)
def on_comboBox_currentIndexChanged(self, index):
self.proxy.setFilterKeyColumn(index)
if __name__ == "__main__":
import sys
app = QtGui.QApplication(sys.argv)
main = myWindow()
main.show()
main.resize(400, 600)
sys.exit(app.exec_())
When i run the above code i get the below output
When i click on Column 2 Header the filter list displayed is as below and its showing correctly (Unique values in that column)...
When i select row 0 col 1 in the displayed filter, i get the following filtered list
But again when i click on the Column 2 header for filter it shows the same list as my 1st image. All the unique items of column 2 (from model view) and not from proxyfilter. Actually it should show only row 0 col 1 as unique items in Column 2 are only row 0 col 1

It is because you are still using the source model to look up the rows. Data in the original model does not change. Only the filter proxy model reflects the filtered changes.
So you just need to modify your lookup in your (really long named) on_view_horizontalHeader_sectionClicked slot:
valuesUnique = [
self.proxy.index(row, self.logicalIndex).data().toString()
for row in xrange(self.proxy.rowCount())
]
You could also remove a few of the conversion for the unique set:
valuesUnique = set(
self.proxy.index(row, self.logicalIndex).data().toString()
for row in xrange(self.proxy.rowCount())
)
...
for actionNumber, actionName in enumerate(sorted(valuesUnique)):
...
And just a small other thing I wanted to point out. You are keeping around a couple of temp objects that don't ever get deleted. In that same slot, you create a new QMenu and QSignalMapper every time but you never clean up the old ones. Over time you are just making more and more.
For the QMenu, just make it a local variable and don't parent it to self. That way it will get cleaned up after it disappears. And for the QSignalMapper, you can just use a deleteLater call before creating a new one:
# local variable, and no parent
menuValues = QtGui.QMenu()
# delete the previous one
try:
self.signalMapper.deleteLater()
except:
pass
self.signalMapper = QtCore.QSignalMapper(self)

Related

How do I add a new set of values on top of the ones I generate in my existing code in Python?

I'd like to be able to add a new tuple on top of the ones already generated in col1, col2 and col3 for a and b datasets, respectively. I have been looking into various loop setups and list comprehensions, yet I do not seem to find what I am looking for. This is the code I am currently using to generate two tuples (you'll have to excuse the potentially clumsy beginner approach):
kv = '''
<Launch>:
BoxLayout:
Button:
size:(80,80)
size_hint:(None,None)
text:"......"
on_press: root.build()
'''
class MyDf(App):
def __init__(self):
super().__init__()
self.a_num = []
a = pd.DataFrame(columns=['col1', 'col2', 'col3'])
a['col1'] = pd.DataFrame(np.random.randint(0, 50, size=1))
a['col2'] = pd.DataFrame(np.random.randint(0, 50, size=1))
a['col3'] = pd.DataFrame(np.random.rand(1))
self.a_num = pd.DataFrame(a)
self.a_limit = a[(a.col3) < 1 & (a.col3 > 0.8)]
self.a_df = tuple(a.itertuples(index=False, name=None))
self.a_arrayed = np.array(self.a_num)
b = pd.DataFrame(columns=['col1', 'col2', 'col3'])
b['col1'] = pd.DataFrame(np.random.randint(0, 50, size=1))
b['col2'] = pd.DataFrame(np.random.randint(0, 50, size=1))
b['col3'] = pd.DataFrame(np.random.rand(1))
self.b_num = pd.DataFrame(b)
self.b_limit = b[(a.col3) < 1 & (b.col3 > 0.8)]
self.b_df = tuple(a.itertuples(index=False, name=None))
self.b_arrayed = np.array(self.b_num)
self.a_arrayed = self.a_num.append(b)
def press(self,instance):
from pprint import pprint
pprint(self.a_arrayed) # self.df, self.limit,
def build(self):
butt=Button(text="...")
butt.bind(on_press=self.press) #dont use brackets while calling function
return butt
MyDf().run()
The code subsequently laumches a kivy window showing a big button. Pressing that button code delivers randomly defined values displayed in above listed col's, e.g.
col1 col2 col3
0 46 20 0.501363
0 41 10 0.515129
col1 col2 col3
0 46 20 0.501363
0 41 10 0.515129
My problem is that it repeats the same values every time I press the kivy button, and I want it to add a new randomly defined set of values for every time I click the kivy button. I assume kv code is not relevant here, since solution to my issue is solely nested in some kind of 'for i in ....range' setup.
Any support out there is appreciated...;o)

Aligning QGridLayouts

I'm trying to create a 'table' where the headers and row labels remain visible. Kind of like the Frozen Column Example, but using a QGridLayout.
My approach is to put three QGridLayouts into aligned QScrollAreas, where the main table contains RxC rows and columns and is scrollable in any direction, and the column and row headers are contained in 1xC and Rx1 cells, respectively. Then I can just get the different QScrollAreas to track each other.
I've got this working (though I haven't touched the matched-scrolling bit yet), but I'm having a lot of trouble getting the three different QGridLayouts to have the same size cells (in width or height).
Part of the problem is that the table cells can be variously sized, meaning that each row and column is not the same height/width as all of the others. But I also can't figure out how to get the heights/widths of the rows of the different QGridLayouts to match.
As you can see in the image above, there are three rows and two columns. I want the Row Headers to line up with the three rows, and the Column Headers to line up with the two columns.
Here's a minimal example. There's a lot still for me to figure out and get working, but this first step is an important one.
import PyQt5.QtGui
import PyQt5.QtWidgets
import PyQt5.QtCore
class MainBuyerWindow(PyQt5.QtWidgets.QMainWindow):
def __init__(self, *args, **kwargs):
super(PyQt5.QtWidgets.QMainWindow, self).__init__(*args, **kwargs)
(
columns_widget, rows_widget, table_widget,
columns_layout, rows_layout, table_layout,
) = self.setup_table()
self.columns_scroll_widget = PyQt5.QtWidgets.QScrollArea()
self.rows_scroll_widget = PyQt5.QtWidgets.QScrollArea()
self.table_scroll_widget = PyQt5.QtWidgets.QScrollArea()
self.columns_scroll_widget.setWidget(columns_widget)
self.rows_scroll_widget.setWidget(rows_widget)
self.table_scroll_widget.setWidget(table_widget)
central_layout = PyQt5.QtWidgets.QGridLayout()
central_layout.addWidget(self.columns_scroll_widget, 0, 1, 1, 5)
central_layout.addWidget( self.rows_scroll_widget, 1, 0, 5, 1)
central_layout.addWidget( self.table_scroll_widget, 1, 1, 5, 5)
central_widget = PyQt5.QtWidgets.QWidget()
central_widget.setLayout(central_layout)
self.setCentralWidget(central_widget)
self.show()
def setup_table(self):
columns_layout = PyQt5.QtWidgets.QGridLayout()
rows_layout = PyQt5.QtWidgets.QGridLayout()
table_layout = PyQt5.QtWidgets.QGridLayout()
columns_widget = PyQt5.QtWidgets.QWidget()
columns_widget.setLayout(columns_layout)
rows_widget = PyQt5.QtWidgets.QWidget()
rows_widget.setLayout(rows_layout)
table_widget = PyQt5.QtWidgets.QWidget()
table_widget.setLayout(table_layout)
table_layout.addWidget(PyQt5.QtWidgets.QLabel("This cell\nis a\ntall one"), 0, 0)
table_layout.addWidget(PyQt5.QtWidgets.QLabel("This cell is shorter"), 1, 0)
table_layout.addWidget(PyQt5.QtWidgets.QLabel("This cell is of\nmedium height"), 2, 0)
table_layout.addWidget(PyQt5.QtWidgets.QLabel("Also notice that widths of columns are not all the same"), 0, 1)
table_layout.addWidget(PyQt5.QtWidgets.QLabel("Though that doesn't matter"), 1, 1)
table_layout.addWidget(PyQt5.QtWidgets.QLabel("(In this example anyway)"), 2, 1)
columns_layout.addWidget(PyQt5.QtWidgets.QLabel("Column Header 1"), 0, 0)
columns_layout.addWidget(PyQt5.QtWidgets.QLabel("Column Header 2"), 0, 1)
rows_layout.addWidget(PyQt5.QtWidgets.QLabel("Row Header 1"), 0, 0)
rows_layout.addWidget(PyQt5.QtWidgets.QLabel("Row Header 2"), 1, 0)
rows_layout.addWidget(PyQt5.QtWidgets.QLabel("Row Header 3"), 2, 0)
return (
columns_widget, rows_widget, table_widget,
columns_layout, rows_layout, table_layout,
)
######################################################################
if __name__ == "__main__":
app = PyQt5.QtWidgets.QApplication([])
window = MainBuyerWindow()
app.exec_()
I think my main problem is I don't understand Qt well enough, so I'm not sure what to look at. What I tried was calling the show() (or activate()) method to get the geometries all worked out, and then going through the first row and column of the table_layout to get their dimensions using cellRect, and then calling columnMinimumWidth and rowMinimumHeight on the corresponding row/column of rows_layout and columns_layout.
This is the same idea used in this (almost identical) question, but when I implement it here it also doesn't seem to work. (Essentially just adding the following lines)
table_layout.activate()
for i in range(table_layout.columnCount()):
w = table_layout.columnMinimumWidth(i)
columns_layout.setColumnMinimumWidth(i, w)
for i in range(table_layout.rowCount()):
h = table_layout.rowMinimumHeight(i)
rows_layout.setRowMinimumHeight(i, h)
Can someone help me out here? Thank you so much!
I would set a fixed height to each row header label based on the height of that row in the table (and similarly a fixed width to each column header label based on the width of that column in the table), which fixes the layout alignment but also makes it simple to match up the scrolling.
class MainBuyerWindow(PyQt5.QtWidgets.QMainWindow):
def __init__(self, *args, **kwargs):
super(PyQt5.QtWidgets.QMainWindow, self).__init__(*args, **kwargs)
(
columns_widget, rows_widget, table_widget,
columns_layout, rows_layout, table_layout,
) = self.setup_table()
self.columns_scroll_widget = PyQt5.QtWidgets.QScrollArea()
self.rows_scroll_widget = PyQt5.QtWidgets.QScrollArea()
self.table_scroll_widget = PyQt5.QtWidgets.QScrollArea()
self.columns_scroll_widget.setWidget(columns_widget)
self.rows_scroll_widget.setWidget(rows_widget)
self.table_scroll_widget.setWidget(table_widget)
for i in range(rows_layout.count()):
label = rows_layout.itemAt(i).widget()
label.setFixedHeight(table_layout.cellRect(i, 0).height())
for i in range(columns_layout.count()):
label = columns_layout.itemAt(i).widget()
label.setFixedWidth(table_layout.cellRect(0, i).width())
...
Use QScrollArea.setWidgetResizable(True) so the scroll area can resize the widget as needed, and allow the header labels to take up the required amount of space to be aligned with the table.
...
self.columns_scroll_widget.setWidgetResizable(True)
self.rows_scroll_widget.setWidgetResizable(True)
self.table_scroll_widget.setWidgetResizable(True)
# Size policies to keep the row and column headers minimal/sufficient
# and allow table_scroll_widget to get as much space as possible when resized
self.columns_scroll_widget.setSizePolicy(PyQt5.QtWidgets.QSizePolicy.Preferred, PyQt5.QtWidgets.QSizePolicy.Minimum)
self.rows_scroll_widget.setSizePolicy(PyQt5.QtWidgets.QSizePolicy.Minimum, PyQt5.QtWidgets.QSizePolicy.Preferred)
...
QScrollBar inherits from QAbstractSlider so you can connect its valueChanged signal to the setValue slot of another scroll bar. These values will be consistent across the layouts now since their row heights and column widths are equal.
...
# User scrolling in table will cause headers to scroll
self.table_scroll_widget.horizontalScrollBar().valueChanged[int].connect(self.columns_scroll_widget.horizontalScrollBar().setValue)
self.table_scroll_widget.verticalScrollBar().valueChanged[int].connect(self.rows_scroll_widget.verticalScrollBar().setValue)
# User scrolling on headers will cause table to scroll
self.columns_scroll_widget.horizontalScrollBar().valueChanged[int].connect(self.table_scroll_widget.horizontalScrollBar().setValue)
self.rows_scroll_widget.verticalScrollBar().valueChanged[int].connect(self.table_scroll_widget.verticalScrollBar().setValue)
central_layout = PyQt5.QtWidgets.QGridLayout()
central_layout.addWidget(self.columns_scroll_widget, 0, 1, 1, 5)
central_layout.addWidget( self.rows_scroll_widget, 1, 0, 5, 1)
central_layout.addWidget( self.table_scroll_widget, 1, 1, 5, 5)
central_widget = PyQt5.QtWidgets.QWidget()
central_widget.setLayout(central_layout)
self.setCentralWidget(central_widget)
self.show()
Outcome:

Format certain rows after writing to excel file

I have some code which compares two excel files and determines any new rows (new_rows) added or any rows which were deleted (dropped_rows). It then uses xlsxwriter to write this to a excel sheet. The bit of code I am having trouble with is that it is supposed to then iterate through the rows and if the row was a new row or a dropped row it is supposed to format it a certain way. For whatever reason this part of the code isn't working correct and being ignored.
I've tried a whole host of different syntax to make this work but no luck.
UPDATE
After some more trial and error the issue seems to be caused by the index column. It is a Case Number column and the values have a prefix like "Case_123, Case_456, Case_789, etc..". This seems to be the root of the issue. But not sure how to solve for it.
grey_fmt = workbook.add_format({'font_color': '#E0E0E0'})
highlight_fmt = workbook.add_format({'font_color': '#FF0000', 'bg_color':'#B1B3B3'})
new_fmt = workbook.add_format({'font_color': '#32CD32','bold':True})
# set format over range
## highlight changed cells
worksheet.conditional_format('A1:J10000', {'type': 'text',
'criteria': 'containing',
'value':'→',
'format': highlight_fmt})
# highlight new/changed rows
for row in range(dfDiff.shape[0]):
if row+1 in newRows:
worksheet.set_row(row+1, 15, new_fmt)
if row+1 in droppedRows:
worksheet.set_row(row+1, 15, grey_fmt)
the last part # highlight new/changed rows is the bit that is not working. The conditional format portion works fine.
the rest of the code:
import pandas as pd
from pathlib import Path
def excel_diff(path_OLD, path_NEW, index_col):
df_OLD = pd.read_excel(path_OLD, index_col=index_col).fillna(0)
df_NEW = pd.read_excel(path_NEW, index_col=index_col).fillna(0)
# Perform Diff
dfDiff = df_NEW.copy()
droppedRows = []
newRows = []
cols_OLD = df_OLD.columns
cols_NEW = df_NEW.columns
sharedCols = list(set(cols_OLD).intersection(cols_NEW))
for row in dfDiff.index:
if (row in df_OLD.index) and (row in df_NEW.index):
for col in sharedCols:
value_OLD = df_OLD.loc[row,col]
value_NEW = df_NEW.loc[row,col]
if value_OLD==value_NEW:
dfDiff.loc[row,col] = df_NEW.loc[row,col]
else:
dfDiff.loc[row,col] = ('{}→{}').format(value_OLD,value_NEW)
else:
newRows.append(row)
for row in df_OLD.index:
if row not in df_NEW.index:
droppedRows.append(row)
dfDiff = dfDiff.append(df_OLD.loc[row,:])
dfDiff = dfDiff.sort_index().fillna('')
print(dfDiff)
print('\nNew Rows: {}'.format(newRows))
print('Dropped Rows: {}'.format(droppedRows))
# Save output and format
fname = '{} vs {}.xlsx'.format(path_OLD.stem,path_NEW.stem)
writer = pd.ExcelWriter(fname, engine='xlsxwriter')
dfDiff.to_excel(writer, sheet_name='DIFF', index=True)
df_NEW.to_excel(writer, sheet_name=path_NEW.stem, index=True)
df_OLD.to_excel(writer, sheet_name=path_OLD.stem, index=True)
# get xlsxwriter objects
workbook = writer.book
worksheet = writer.sheets['DIFF']
worksheet.hide_gridlines(2)
worksheet.set_default_row(15)
# define formats
date_fmt = workbook.add_format({'align': 'center', 'num_format': 'yyyy-mm-dd'})
center_fmt = workbook.add_format({'align': 'center'})
number_fmt = workbook.add_format({'align': 'center', 'num_format': '#,##0.00'})
cur_fmt = workbook.add_format({'align': 'center', 'num_format': '$#,##0.00'})
perc_fmt = workbook.add_format({'align': 'center', 'num_format': '0%'})
grey_fmt = workbook.add_format({'font_color': '#E0E0E0'})
highlight_fmt = workbook.add_format({'font_color': '#FF0000', 'bg_color':'#B1B3B3'})
new_fmt = workbook.add_format({'font_color': '#32CD32','bold':True})
# set format over range
## highlight changed cells
worksheet.conditional_format('A1:J10000', {'type': 'text',
'criteria': 'containing',
'value':'→',
'format': highlight_fmt})
# highlight new/changed rows
for row in range(dfDiff.shape[0]):
if row+1 in newRows:
worksheet.set_row(row+1, 15, new_fmt)
if row+1 in droppedRows:
worksheet.set_row(row+1, 15, grey_fmt)
# save
writer.save()
print('\nDone.\n')
def main():
path_OLD = Path('file1.xlsx')
path_NEW = Path('file2.xlsx')
# get index col from data
df = pd.read_excel(path_NEW)
index_col = df.columns[0]
print('\nIndex column: {}\n'.format(index_col))
excel_diff(path_OLD, path_NEW, index_col)
if __name__ == '__main__':
main()

xlrd named range example?

I have an excel spreadsheet that I am trying to parse with xlrd. The spreadsheet itself makes extensive use of named ranges.
If I use:
for name in book.name_map:
print(name)
I can see all of the names are there.
However I can't make any of the methods work (cell method and area2d). Can anyone give me an example of the syntax to be able to read the cell range that a name is pointing to given the name.
The Excel file is an XLSM file with lots of visual basic that also operates on these named ranges.
I think that the naming support in XLRD is broken for XLSM files but I found an answer by switching to openpyxl. This has a function get_named_ranges() which contains all of the named ranges. The support after that is a bit thin so I wrote my own class to turn the named ranges in my spreadsheet into a class where I can access the same information using the same names.
# -- coding: utf-8 --
"""
Created on Wed Sep 14 09:42:09 2016
#author: ellwood
"""
from openpyxl import load_workbook
class NamedArray(object):
''' Named range object
'''
C_CAPS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
def __init__(self, workbook, named_range_raw):
''' Initialise a NameArray object from the named_range_raw information in the given
workbook
'''
self.wb = workbook
sheet_str, cellrange_str = str(named_range_raw).split('!')
self.sheet =sheet_str.split('"')[1]
self.loc = self.wb[self.sheet]
if ':' in cellrange_str:
self.has_range = True
self.has_value = False
lo,hi = cellrange_str.split(':')
self.ad_lo = lo.replace('$','')
self.ad_hi = hi.replace('$','')
else:
self.has_range = False
self.has_value = True
self.ad_lo = cellrange_str.replace('$','')
self.ad_hi = self.ad_lo
self.min_row = self.get_row(self.ad_lo)
self.max_row = self.get_row(self.ad_hi)
self.rows = self.max_row - self.min_row + 1
self.min_col = self.col_to_n(self.ad_lo)
self.max_col = self.col_to_n(self.ad_hi)
self.cols = self.max_col - self.min_col + 1
def size_of(self):
''' Returns two dimensional size of named space
'''
return self.cols, self.rows
def cols(self):
''' Returns number of cols in named space
'''
return self.cols
def rows(self):
''' Returns number of rows in named space
'''
return self.rows
def value(self, r=1, c=1):
''' Returns the value at row r, column c
'''
if self.has_value:
return self.loc.cell(self.ad_lo).value
assert r < self.max_rows
assert c < self.max_cols
return self.loc.cell(self.n_to_col(self.min_col + c-1)+str(self.min_row+r-1)).value
def is_range(self):
''' if true then name defines a table more than 1 cell
'''
return self.range
def is_value(self):
''' if true then name defines the location of a single value
'''
return None
def __str__(self):
''' printed description of named space
'''
locs = 's ' + self.ad_lo + ':' + self.ad_hi if self.is_range else ' ' + self.ad_lo
return('named range'+ str(self.size_of()) + ' in sheet ' + self.sheet + ' # location' + locs)
#classmethod
def get_row(cls, ad):
''' get row number from cell string
Cell string is assumed to be in excel format i.e "ABC123" where row is 123
'''
row = 0
for l in ad:
if l in "1234567890":
row = row*10 + int(l)
return row
#classmethod
def col_to_n(cls, ad):
''' find column number from xl address
Cell string is assumed to be in excel format i.e "ABC123" where column is abc
column number is integer represenation i.e.(A-A)*26*26 + (B-A)*26 + (C-A)
'''
n = 0
for l in ad:
if l in cls.C_CAPS:
n = n*26 + cls.C_CAPS.find(l)+1
return n
#classmethod
def n_to_col(cls,n):
''' make xl column address from column number
'''
ad = ''
while n > 0:
ad = cls.C_CAPS[n%26-1] + ad
n = n // 26
return ad
class Struct(object):
''' clast which turns a dictionary into a structure
'''
def __init__(self, **entries):
self.__dict__.update(entries)
def repr__(self):
return '<%s>' % str('\n '.join('%s : %s' % (k, repr(v)) for (k, v) in self.__dict.iteritems()))
def get_names(workbook):
''' Get a structure containing all of the names in the workbook
'''
named_ranges = wb.get_named_ranges()
name_list = {}
for named_range in named_ranges:
name = named_range.name
if name[0:2] == 'n_':
# only store the names beginning with 'n_'
name_list[name[2:]] = NamedArray(wb, str(named_range))
for item in name_list:
print (item, '=', name_list[item])
return Struct(**name_list)
# ------------------
# program example
# -----------------
wb = load_workbook('test.xlsm', data_only=True)
n = get_names(wb)
print(n.my_name.value())
One Small optimisation is that I prefixed all of the names I was interested in importing wiht 'n_' so I could then ignore any built in Excel names. I hope this is useful to someone.

wxPython wxListCtrl selected row color

I want to have certain rows selected color be red instead of the standard color (blue on windows) so that I can indicate status. Anyone know if this is possible in wxPython?
In your class that you derive from wx.ListCtrl, take a look at overriding
def OnGetItemAttr(self, item):
return self.normalAttr[item % 2]
#
Where the item attributes are initialized ahead of time using:
self.normalAttr = []
self.normalAttr.append(wx.ListItemAttr())
grayAttr = wx.ListItemAttr()
grayAttr.SetBackgroundColour(lightGray)
self.normalAttr.append(grayAttr)
So in this case, I'm alternating background colors between the default, and a light Gray attribute.
This function is called for each row as its painted, so you can use it to indicate all sorts of status. If row is selected should be an easy case.
In order to do what you want, i.e. have a different selection color when certain items are selected, you will need to drop into win32. Fortunately, it is not too hard to do that in python. It does however make your code platform dependent. I tried it out today in a small program. If the Genre is not "Rock" I make the selection orange. Here are some screenshots.
Rock Items Selected
Mixed Items Selected. Notice the RnB and Blues are selected with Orange
alt text http://img258.imageshack.us/img258/1307/soshot2.jpg
Here is the code. It looks scary at first but if you know any win32, its not that bad. I make use of the pywin32 package and the std ctypes libraries. I had to define some of the SDK constants as they were not available in the win32con module.
import sys
import wx
import wx.lib.mixins.listctrl as listmix
import win32api
import win32gui
import win32con
import win32gui_struct
import commctrl
import ctypes
from ctypes.wintypes import BOOL, HWND, RECT, UINT, DWORD, HDC, DWORD, LPARAM, COLORREF
LVM_FIRST = 0x1000
LVM_GETSUBITEMRECT=(LVM_FIRST + 56)
LVIR_BOUNDS =0
LVIR_ICON =1
LVIR_LABEL =2
LVIR_SELECTBOUNDS =3
DEFAULT_GUI_FONT =17
#LPNMHDR
class NMHDR(ctypes.Structure):
pass
INT = ctypes.c_int
NMHDR._fields_ = [('hwndFrom', HWND), ('idFrom', UINT), ('code', INT)]
LPNMHDR = ctypes.POINTER(NMHDR)
#LPNMCUSTOMDRAW
class NMCUSTOMDRAW(ctypes.Structure):
pass
NMCUSTOMDRAW._fields_ = [('hdr', NMHDR), ('dwDrawStage', DWORD), ('hdc', ctypes.c_int),
('rc', RECT), ('dwItemSpec', DWORD), ('uItemState', UINT),
('lItemlParam', LPARAM)]
LPNMCUSTOMDRAW = ctypes.POINTER(NMCUSTOMDRAW)
#LPNMLVCUSTOMDRAW
class NMLVCUSTOMDRAW(ctypes.Structure):
pass
NMLVCUSTOMDRAW._fields_ = [('nmcd', NMCUSTOMDRAW),
('clrText', COLORREF),
('clrTextBk', COLORREF),
('iSubItem', ctypes.c_int),
('dwItemType', DWORD),
('clrFace', COLORREF),
('iIconEffect', ctypes.c_int),
('iIconPhase', ctypes.c_int),
('iPartId', ctypes.c_int),
('iStateId', ctypes.c_int),
('rcText', RECT),
('uAlign', UINT)
]
LPNMLVCUSTOMDRAW = ctypes.POINTER(NMLVCUSTOMDRAW)
musicdata = {
1 : ("Bad English", "The Price Of Love", "Rock"),
2 : ("DNA featuring Suzanne Vega", "Tom's Diner", "Rock"),
3 : ("George Michael", "Praying For Time", "Rock"),
4 : ("Gloria Estefan", "Here We Are", "Rock"),
5 : ("Linda Ronstadt", "Don't Know Much", "Rock"),
6 : ("Michael Bolton", "How Am I Supposed To Live Without You", "Blues"),
7 : ("Paul Young", "Oh Girl", "Rock"),
8 : ("Paula Abdul", "Opposites Attract", "Rock"),
9 : ("Richard Marx", "Should've Known Better", "Rock"),
10 : ("Bobby Brown", "My Prerogative", "RnB"),
}
class MyListCtrl(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin):
def __init__(self, parent, ID, pos=wx.DefaultPosition,
size=wx.DefaultSize, style=0):
wx.ListCtrl.__init__(self, parent, ID, pos, size, style)
listmix.ListCtrlAutoWidthMixin.__init__(self)
def ShouldCustomDraw(self, row):
if self.IsSelected(row):
listitem = self.GetItem(row, 2)
genre = listitem.GetText()
return genre != "Rock"
def CustomDraw(self, lpcd):
if lpcd.contents.nmcd.dwDrawStage == commctrl.CDDS_PREPAINT:
return (True, commctrl.CDRF_NOTIFYITEMDRAW)
if lpcd.contents.nmcd.dwDrawStage == commctrl.CDDS_ITEMPREPAINT:
if self.ShouldCustomDraw(lpcd.contents.nmcd.dwItemSpec):
#do custom drawing for non Rock selected rows
#paint the selection background
color = win32api.RGB(255, 127, 0) #orange
brush = win32gui.CreateSolidBrush(color)
r = lpcd.contents.nmcd.rc
win32gui.FillRect(int(lpcd.contents.nmcd.hdc), (r.left+4, r.top, r.right, r.bottom), brush)
win32gui.DeleteObject(brush)
return (True, commctrl.CDRF_NOTIFYSUBITEMDRAW)
if lpcd.contents.nmcd.dwDrawStage == commctrl.CDDS_ITEMPREPAINT|commctrl.CDDS_SUBITEM:
row = lpcd.contents.nmcd.dwItemSpec
col = lpcd.contents.iSubItem
item = self.GetItem(row, col)
text = item.GetText()
#paint the text
rc = RECT()
rc.top = col
if col > 0:
rc.left = LVIR_BOUNDS
else:
rc.left = LVIR_LABEL
success = win32api.SendMessage(self.Handle, LVM_GETSUBITEMRECT, row, ctypes.addressof(rc))
if col > 0:
rc.left += 5
else:
rc.left += 2
rc.top += 2
if success:
oldColor = win32gui.SetTextColor(lpcd.contents.nmcd.hdc, win32gui.GetSysColor(win32con.COLOR_HIGHLIGHTTEXT))
win32gui.DrawText(lpcd.contents.nmcd.hdc, text, len(text), (rc.left, rc.top, rc.right, rc.bottom), win32con.DT_LEFT|win32con.DT_VCENTER)
win32gui.SetTextColor(lpcd.contents.nmcd.hdc, oldColor)
return (True, commctrl.CDRF_SKIPDEFAULT)
# don't need custom drawing
return (True, commctrl.CDRF_DODEFAULT)
class MyFrame(wx.Frame):
def __init__(self, *args, **kwds):
wx.Frame.__init__(self, *args, **kwds)
self._sizer = wx.BoxSizer(wx.VERTICAL)
tID = wx.NewId()
self._ctl = MyListCtrl(self, tID,
style=wx.LC_REPORT
#| wx.BORDER_SUNKEN
| wx.BORDER_NONE
| wx.LC_EDIT_LABELS
| wx.LC_SORT_ASCENDING
#| wx.LC_NO_HEADER
#| wx.LC_VRULES
#| wx.LC_HRULES
#| wx.LC_SINGLE_SEL
)
self._sizer.Add(self._ctl, 1, wx.EXPAND, 3)
self.PopulateList()
self.oldWndProc = win32gui.SetWindowLong(self.GetHandle(), win32con.GWL_WNDPROC, self.MyWndProc)
def MyWndProc(self, hWnd, msg, wParam, lParam):
if msg == win32con.WM_NOTIFY:
hwndFrom, idFrom, code = win32gui_struct.UnpackWMNOTIFY(lParam)
if code == commctrl.NM_CUSTOMDRAW and hwndFrom == self._ctl.Handle:
lpcd = ctypes.cast(lParam, LPNMLVCUSTOMDRAW)
retProc, retCode = self._ctl.CustomDraw(lpcd)
if retProc:
return retCode
# Restore the old WndProc. Notice the use of wxin32api
# instead of win32gui here. This is to avoid an error due to
# not passing a callable object.
if msg == win32con.WM_DESTROY:
win32api.SetWindowLong(self.GetHandle(),
win32con.GWL_WNDPROC,
self.oldWndProc)
# Pass all messages (in this case, yours may be different) on
# to the original WndProc
return win32gui.CallWindowProc(self.oldWndProc,
hWnd, msg, wParam, lParam)
def PopulateList(self):
self._ctl.InsertColumn(0, "Artist")
self._ctl.InsertColumn(1, "Title")
self._ctl.InsertColumn(2, "Genre")
items = musicdata.items()
for key, data in items:
index = self._ctl.InsertStringItem(sys.maxint, data[0])
self._ctl.SetStringItem(index, 1, data[1])
self._ctl.SetStringItem(index, 2, data[2])
self._ctl.SetItemData(index, key)
self._ctl.SetColumnWidth(0, wx.LIST_AUTOSIZE)
self._ctl.SetColumnWidth(1, wx.LIST_AUTOSIZE)
self._ctl.SetColumnWidth(2, 100)
self.currentItem = 0
class MyApp(wx.App):
def OnInit(self):
frame = MyFrame(None, -1, 'wxListCtrl StackOverflow')
frame.Show()
self.SetTopWindow(frame)
return 1
if __name__ == "__main__":
app = MyApp(0)
app.MainLoop()

Resources