PDA

View Full Version : Draw Grid lines with Painter



mechsin
10th January 2013, 19:57
I want to draw grid lines for only some boxes in my QTableView. For this I have implemented a delegate that draws the lines. This all works however when I draw the lines they are thick and black. How do I get a hold of the default settings for the grid lines from my view that way I can make it look like all my other QTableview grid lines? My code is below.



class setup_delegate(QtGui.QStyledItemDelegate):


def __init__(self, parent=None):
super(setup_delegate, self).__init__(parent)

def paint(self, painter, option, index):
if index.data().toBool() or index.row() == 0:
painter.save()
painter.drawRect(option.rect)
painter.restore()
QtGui.QStyledItemDelegate.paint(self, painter, option, index)

mechsin
16th January 2013, 19:51
Ok so this isn't as simple as I initially though. The code in the above post draws lines but it doesn't acutally draw them in the right place. Reading the documentation carfully drawRect atually



Draws the current rectangle with the current pen and brush.

A filled rectangle has a size of rectangle.size(). A stroked rectangle has a size of rectangle.size() plus the pen width.



So because the size of the rect is plus the pen width it looks like my grid lines are actually being drawn outside of my "cell". I think this is causing me problems because when a cell whose gridlines aren't being drawn is filled with some color this causes the grid lines for the column next to it to be covered. I made as short an example as I can. Its below some one please respond I could really use some help on this. Take a look at the cells with alternative color backgrounds.


import random
from PyQt4 import QtCore, QtGui

model_idx = QtCore.QModelIndex


class TestModel(QtCore.QAbstractTableModel):

def __init__(self, dataset, parent=None):
super(TestModel, self).__init__(parent)
self.ds = dataset

def columnCount(self, index=model_idx()):
return len(self.ds.keys())

def rowCount(self, index=model_idx()):
return max(map(len, self.ds.values()))

def data(self, index, role=QtCore.Qt.DisplayRole):
row = index.row()
col = index.column()
field = self.ds.keys()[col]
if (role == QtCore.Qt.DisplayRole) or (role == QtCore.Qt.EditRole):
values = self.ds[field]
if row == 0:
k = None
elif len(values) > (row - 1):
k = values[row - 1]
else:
k = None
return QtCore.QVariant(k)
return QtCore.QVariant()

def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if orientation == QtCore.Qt.Horizontal:
if role == QtCore.Qt.DisplayRole:
name = self.ds.keys()[section]
return QtCore.QVariant(name)
return QtCore.QVariant()


class TestDelegate(QtGui.QStyledItemDelegate):

def __init__(self, parent=None):
super(TestDelegate, self).__init__(parent)

def paint(self, painter, option, index):
if index.data().toBool() or index.row() == 0:
painter.save()
painter.drawRect(option.rect)
super(TestDelegate, self).paint(painter, option, index)
painter.restore()

def SetupData():
random.seed(92)
space = xrange(5000)
data = {}
for k in range(5):
key = 'Data Field {}'.format(k)
ds = random.sample(space, 10)
num_toss = random.randint(0,10)
for it in range(num_toss):
idx = random.randint(0,len(ds) - 1)
ds.pop(idx)
data[key] = ds
return data

if __name__ == "__main__":
import sys
app = QtGui.QApplication(sys.argv)
data = SetupData()
form = QtGui.QTableView()
model = TestModel(data, form)
delegate = TestDelegate(form)

form.setShowGrid(False)
form.setAlternatingRowColors(True)
form.setModel(model)
form.setItemDelegate(delegate)
form.show()
sys.exit(app.exec_())

wysota
17th January 2013, 02:32
I'm not sure the delegate is the right place to draw the grid. The original grid is drawn by the table view itself. If you want to do it in the delegate then I suppose you have to adjust the rectangle covered by the fill and possibly by the grid itself when the grid is being drawn. An alternative is to not paint the grid in the delegate's paint routine but rather subclass the view, have it do its original job (that calls paint on the delegate) and then call the delegate for each cell again just to overpaint the grid.

mechsin
17th January 2013, 21:13
Thanks Wysota after digging around in the source code myself I came to the same conclusion. That is that it would be better to subclass the view and then re-implement the paintEvent method. I did get the grid lines to draw using the delegate by resizing the rectangle like you suggested but in the end this approach still had problems because the delegate isn't always getting called when the background is getting redrawn so sometimes my gridlines will disappear and not come back till I click in the box or some other event happens that calls the delegate. I am going to work on my implementation using the view and paintEvent method I also have to work out how to deal with the alternating back ground color I am not for sure yet when that is getting painted but I do know that I need to paint it in the paintEvent.

If I work something out I will be sure to post it.

Thanks

wysota
17th January 2013, 22:37
The background is painted by the delegate, as far as I remember. The delegate checks for the QStyleOptionViewItemV2::Alternate feature and decides which background to draw.

mechsin
22nd January 2013, 21:23
Wysota you are correct the background of the item/cell is painted by the delegate. However from digging around in the source code I found that the alternating background is drawn by something else. In the C++ source code I found a private method call drawCell this is the function that does the final setup to the Style Item Options object and then calls the delegate. But just before it calls the delegate it does this



q->style()->drawPrimitive(QStyle::PE_PanelItemViewRow, &opt, painter, q);


I am pretty sure that this is when the alternate base background gets painted. Of course I am really no good at C++ so I could be wrong but if I sub the delegate and then overwrite the paint method and just do pass the alternate background colors will still get painted at least in PyQt.

In any case the paintEvent is where the grid lines are getting drawn. So I sub classed the view and then over wrote that. It is a lot of work to get the math set up right and I had to duplicate a lot of code that was already in the source but the final result is what I deemed a SparseTableView. If I was better at C++ I would make suggestions to change the code so that this doesn't have to be such a chore, maybe in the future. I would have liked if the drawCell method were made public and the grid drawing done there. Anyway I posted my example here in PyQt of course in case anyone else needs it. It is well explained in the comments but basically if a cell doesn't have data I don't paint the grid lines. One caveat is this view cannot deal with cells that span more than one column or row. It was way more work to get that to happen. If you need that my best advice is to take a look at the QTableView source and then the paintEvent method.



import random
from PyQt4 import QtCore, QtGui

model_idx = QtCore.QModelIndex


class TestModel(QtCore.QAbstractTableModel):

""" Test Setup Data Model

In this model the first row of the table is always empty.
"""

def __init__(self, dataset, parent=None):
super(TestModel, self).__init__(parent)
self.ds = dataset

def columnCount(self, index=model_idx()):
return len(self.ds.keys())

def rowCount(self, index=model_idx()):
return max(map(len, self.ds.values()))

def data(self, index, role=QtCore.Qt.DisplayRole):
row = index.row()
col = index.column()
try:
field = self.ds.keys()[col]
except IndexError:
return QtCore.QVariant()
if (role == QtCore.Qt.DisplayRole) or (role == QtCore.Qt.EditRole):
values = self.ds[field]
if row == 0:
k = None
elif len(values) > (row - 1):
k = values[row - 1]
else:
k = None
return QtCore.QVariant(k)
return QtCore.QVariant()

def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if orientation == QtCore.Qt.Horizontal:
if role == QtCore.Qt.DisplayRole:
name = self.ds.keys()[section]
return QtCore.QVariant(name)
return QtCore.QVariant()

def flags(self, index):
if index.isValid():
return QtCore.Qt.ItemFlags(QtCore.Qt.ItemIsEnabled |
QtCore.Qt.ItemIsSelectable)

class SparseTableView(QtGui.QTableView):

""" Table View With Sparse Grid Lines

This is an example of how you can overide the paintEvent in the QTableView
to draw your own grid lines. In this particular view grid lines won't be
drawn for empty cells with the exception of the first row which if use with
the TestModel is always empty. Note this view cannot deal with cells that
span multiple rows or columns.

"""

def __init__(self, parent=None):
super(SparseTableView, self).__init__(parent)
self.setAlternatingRowColors(True)
self.setShowGrid(False)

def paintEvent(self, event):
# Call the super here first. It will draw a bunch of stuff including
# alternating background color and the text. It will also call the item
# delegate if one is set. We do this first otherwise our gridlines
# might get painted over.
super(SparseTableView, self).paintEvent(event)

# Get all the specifics about the grid color and size and setup the
# grid pen
sh = QtGui.QStyle.SH_Table_GridLineColor
gridHint = self.style().styleHint(sh, self.viewOptions())
# Do this to convert from signed int to unsigned int
gridHint &= 0xffffffff
gridColor = QtGui.QColor.fromRgb(gridHint)
gridSize = 1
gridPen = QtGui.QPen(gridColor, gridSize, self.gridStyle())

# Create the painter
painter = QtGui.QPainter(self.viewport())

# Shortcuts to vertical and horizontal headers
vh = self.verticalHeader()
hh = self.horizontalHeader()

# Get the first and last rows that are visible in the view and if the
# last visiable row returns -1 set it to the row count
firstVisualRow = max([vh.visualIndexAt(0), 0])
lastVisualRow = vh.visualIndexAt(vh.viewport().height())
if lastVisualRow == -1:
lastVisualRow = self.model().rowCount(self.rootIndex()) - 1

# Get the first and last columns that are visible in the view and if
# if the last visible column is -1 set it to the column count.
firstVisualColumn = max([hh.visualIndexAt(0), 0])
lastVisualColumn = hh.visualIndexAt(hh.viewport().width())
if lastVisualColumn == -1:
lastVisualColumn = hh.count() - 1

# Iterate through each row and column drawing only the
# bottom and left side lines for each cell. Skipping rows and columns
# that are hidden
for vrow in range(firstVisualRow, lastVisualRow + 1):
row = vh.logicalIndex(vrow)
FirstRow = (vrow == 0)
if vh.isSectionHidden(row):
continue
# Get top left Y coordinate and row height
rowY = self.rowViewportPosition(row)
rowh = self.rowHeight(row)
for vcol in range(firstVisualColumn, lastVisualColumn + 1):
col = hh.logicalIndex(vcol)
FirstColumn = (vcol == 0)
if hh.isSectionHidden(col):
continue
# Get top left X coordinate and column width
colX = self.columnViewportPosition(col)
colw = self.columnWidth(col)

# Get the model index
index = self.model().createIndex(row, col)

# Specify top, bottom, left and right of the cell accounting
# for the with of the grid pen.
top = rowY
bottom = rowY + rowh - gridSize
left = colX
right = colX + colw - gridSize

# Boolean holder values for if this cell has data or if the
# adjacent cell has data. If the adjacent cell has data. We
# Still need to draw some lines.
cell_has_data = index.data().toBool()
adj_idx = index.model().createIndex(row, col + 1)
adjacent_cell_filled = adj_idx.data().toBool()

# Save the painter and set the pen
painter.save()
painter.setPen(gridPen)

# If the cell is empty erase the back ground that way the
# alternating background colors won't show up
if not cell_has_data:
painter.eraseRect(colX, rowY, colw, rowh)

# Draw Horizontal Lines
# Only do this if the cell has data or is in the first row
# We draw only the bottom line
if cell_has_data or FirstRow:
painter.drawLine(left, bottom, right, bottom)

# Draw Vertical Lines
# Only do this if this cell or the right side adjacent cell has
# data or we are in the first row.
# We draw only the right side line
if cell_has_data or adjacent_cell_filled or FirstRow:
painter.drawLine(right, top, right, bottom)

# Restore painter
painter.restore()

def SetupData():
random.seed(94546)
space = xrange(5000)
data = {}
for k in range(5):
key = 'Data Field {}'.format(k)
ds = random.sample(space, 10)
num_toss = random.randint(0,10)
for it in range(num_toss):
idx = random.randint(0,len(ds) - 1)
ds.pop(idx)
data[key] = ds
return data

if __name__ == "__main__":
import sys
app = QtGui.QApplication.instance()
if not app:
app = QtGui.QApplication(sys.argv)
data = SetupData()
form = SparseTableView()
model = TestModel(data, form)
form.setModel(model)
form.show()
sys.exit(app.exec_())

wysota
22nd January 2013, 21:27
I am pretty sure that this is when the alternate base background gets painted. Of course I am really no good at C++ so I could be wrong but if I sub the delegate and then overwrite the paint method and just do pass the alternate background colors will still get painted at least in PyQt.
You can always overpaint it yourself in the delegate.

kiss-o-matic
31st January 2013, 01:44
What I do is turn grid lines off in the QTableView, and then use the delegate to draw lines when I want.



/* Line for Columns */
QPen grid_pen( Qt::Black, 0, Qt::SolidLine );
painter->setPen( grid_pen );
painter->drawLine( opt.rect.topRight(), opt.rect.bottomRight() );

mechsin
3rd February 2013, 14:33
Kiss that is what I tried first but then then my and it seemed to work but then I found there was some problem with my lines. They didn't always show up. It seems like after I moused over the box sometimes my lines would disappear and sometimes they would stay. I though it had to do with the order in which the lines were being drawn so I reordered some stuff but no dice. Of course I am using Python does this work for you in C++. Keep in mind that my table is "sparse" so there isn't data in every box and if there is no data it shouldn't get grid lines.

Although the though just occurred to me I wonder if I create my own painter in the delegate and draw the line if that would solve the problem. That is basically the only difference between what I am doing in the paintEvent and what I was doing in the delegate.