Source code for brainvisa.tools.displayTitledGrid

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#  This software and supporting documentation are distributed by
#      Institut Federatif de Recherche 49
#      CEA/NeuroSpin, Batiment 145,
#      91191 Gif-sur-Yvette cedex
#      France
#
# This software is governed by the CeCILL license version 2 under
# French law and abiding by the rules of distribution of free software.
# You can  use, modify and/or redistribute the software under the
# terms of the CeCILL license version 2 as circulated by CEA, CNRS
# and INRIA at the following URL "http://www.cecill.info".
#
# As a counterpart to the access to the source code and  rights to copy,
# modify and redistribute granted by the license, users are provided only
# with a limited warranty  and the software's author,  the holder of the
# economic rights,  and the successive licensors  have only  limited
# liability.
#
# In this respect, the user's attention is drawn to the risks associated
# with loading,  using,  modifying and/or developing or reproducing the
# software by the user in light of its specific status of free software,
# that may mean  that it is complicated to manipulate,  and  that  also
# therefore means  that it is reserved for developers  and  experienced
# professionals having in-depth computer knowledge. Users are therefore
# encouraged to load and test the software's suitability as regards their
# requirements in conditions enabling the security of their systems and/or
# data to be ensured and,  more generally, to use and operate it in the
# same conditions as regards security.
#
# The fact that you are presently reading this means that you have had
# knowledge of the CeCILL license version 2 and that you accept its terms.
# import anatomist.threaded.api as ana

from __future__ import print_function
from __future__ import absolute_import
from soma.qt_gui.qt_backend import QtCore, Qt, QtGui
from soma.qt_gui.qt_backend.QtGui import QRadioButton, QPalette, QButtonGroup, QLabel, QFrame, QVBoxLayout, QColor
from soma.qt_gui.qt_backend.uic import loadUi
from brainvisa.processing.qtgui.neuroProcessesGUI import mainThreadActions
from soma.qt_gui.qtThread import MainThreadLife
from functools import partial
import brainvisa.anatomist as ana
from anatomist.cpp.paletteEditor import PaletteEditor
import weakref
from six.moves import range
from six.moves import zip

#------------------------------------------------------------------------------


[docs]def displayTitledGrid(transformationManager, parent, inverseRawColumn, objPathMatrix, rowTitles=['raw_space', "MRI_native_space", "mask", "MNI_space", ], rowColors=['darkOrange', 'blue', "MRI", 'blue', 'magenta'], colTitles=['PET', "MRI", "grey"], windowTitle='View grid', linkWindows='space', overlaidImages=[], mainColormap='B-W LINEAR', overlayColormap='RAINBOW', customOverlayColormap='Blue-White', rowButtonSubTitles=None ): '''Grid of Anatomist views Parameters: linkWindows: str possible values : 'all' | none | row rowColors: list default: ['darkOrange', 'blue', "MRI", 'blue', 'magenta'] orange = rawSpace, blue = mri space, magenta = mni space ''' _mw = mainThreadActions().call(_displayTitledGrid_onGuiThread, transformationManager, parent, inverseRawColumn, objPathMatrix, rowTitles=rowTitles, rowColors=rowColors, colTitles=colTitles, windowTitle=windowTitle, linkWindows=linkWindows, overlaidImages=overlaidImages, mainColormap=mainColormap, overlayColormap=overlayColormap, customOverlayColormap=customOverlayColormap, rowButtonSubTitles=rowButtonSubTitles) mw = MainThreadLife( _mw) # ensure mw destruction takes place in the GUI thread return mw
def _displayTitledGrid_onGuiThread(transformationManager, parent, inverseRawColumn, objPathMatrix, rowTitles, rowColors, colTitles, windowTitle, linkWindows, overlaidImages, mainColormap, overlayColormap, customOverlayColormap, rowButtonSubTitles): # DisplayTitledGrid doit etre construit sur le thread de Gui pour etre sur # que la destruction de la mw se fasse sur le thread de Gui TitledGrid = DisplayTitledGrid( objPathMatrix, parent=parent, mainColormap=mainColormap, overlayColormap=overlayColormap, customOverlayColormap=customOverlayColormap) mw = TitledGrid.display(inverseRawColumn=inverseRawColumn, windowFlag=QtCore.Qt.Window, windowTitle=windowTitle, rowTitles=rowTitles, colTitles=colTitles, rowColors=rowColors, linkWindows=linkWindows, overlaidImages=overlaidImages, rowButtonSubTitles=rowButtonSubTitles)[0] return TitledGrid #------------------------------------------------------------------------------
[docs]class DisplayTitledGrid(QtGui.QWidget): def __init__(self, objPathMatrix, parent=None, mainColormap='B-W LINEAR', overlayColormap='RAINBOW', customOverlayColormap='Blue-White'): super(DisplayTitledGrid, self).__init__(parent) self._main_colormap = mainColormap self._overlay_colormap = overlayColormap self._custom_overlay_colormap = customOverlayColormap self._loadObjectInAnatomist(objPathMatrix, interpolation=False) self._overlaid_images = [] self._overlay_fusions = [] self._custom_overlay_fusions = [] # momoTODO : pas besoin d'une liste, un seul suffit sinon # l'utilisateur s'y perd (selection avec row et column). self._selectedRow = -1 self._selectedColumn = -1 self._row_titles = [] self._col_titles = [] self._paletteEditor = None def __del__(self): # make sure mw is deleted first, otherwise its connected signals still point # to slots (in c++) in self, when self is already destroyed, which # generally results in a crash self.mw.close() del self.mw def display(self, inverseRawColumn=False, windowFlag=QtCore.Qt.Window, windowTitle='Compare', rowTitles=['row_1', 'row_2', 'row_3', 'row_4'], colTitles=['col_1', 'col_2', 'col_3'], rowColors=['darkOrange', 'blue', 'blue', 'magenta'], linkWindows='space', overlaidImages=[], rowButtonSubTitles=None): layout = QtGui.QVBoxLayout(self) self.setLayout(layout) self.mw = self._loadUserInterface() # create self.mw.gridLayout self.mw.setWindowTitle(windowTitle) layout.addWidget(self.mw) self._row_titles = rowTitles self._col_titles = colTitles # self._custom_row_titles = [ x for x in rowTitles ] # load overlay (fusionned) images, and make fusions self._loadOverlayImages(overlaidImages) self._createOverlayFusions() self._addColumnButton(colTitles, inverseRawColumn) self._addRowButton( rowTitles, rowColors, inverseRawColumn, rowButtonSubTitles) # self._createWinFrame(self.mw, self.mw.selectedReferenceLabel) # # momoTODO : meme cadre autour de selected reference self._createAndLinkAnatomistWindowsInMainLayout( linkWindows, inverseRawColumn, 'Sagittal', rowTitles) self.mw.anatomistObjectList = self.anatomistObjectList # momo :ca sert a quoi? # replace individual objects by overlays fusions when applicable self._addObjectOrFusion_inAnatomistWindows() self.mw.comboBox.currentIndexChanged.connect(self._onComboBox_changed) self.mw.mixingSlider.valueChanged.connect(self._onMixingRateChanged) self.mw.maximizeButton.clicked.connect(self._onMaximizeButtonClicked) self.show() return [self.mw] #----------------------------------------------------------------------------- # private : begins with _ #----------------------------------------------------------------------------- def _loadObjectInAnatomist(self, objPathMatrix, interpolation=True): a = ana.Anatomist() self.anatomistObjectList = [] for r in range(0, len(objPathMatrix)): objPathRow = objPathMatrix[r] anaObjRow = [] for c in range(0, len(objPathRow)): objPath = objPathRow[c] if (objPath is not None): obj = a.loadObject(objPath, forceReload=False) if not interpolation: obj.attributed()['volumeInterpolation'] = False obj.setPalette(self._main_colormap) anaObjRow.append(obj) else: anaObjRow.append(None) self.anatomistObjectList.append(anaObjRow) def _loadUserInterface(self): dotIdx = __file__.rindex('.') uiFileName = __file__[:dotIdx] + '.ui' mw = loadUi(uiFileName) mw.mixRate.setText('50 %') return mw def _addColumnButton(self, buttonTitles, inverseRawColumn): for buttonIndex in range(0, len(buttonTitles)): title = buttonTitles[buttonIndex] button = QRadioButton(title) if (inverseRawColumn): self.mw.gridLayout.addWidget( button, buttonIndex + 1, 0, QtCore.Qt.AlignHCenter) self.mw.gridLayout.setRowStretch(buttonIndex + 1, 10) else: self.mw.gridLayout.addWidget( button, 0, buttonIndex + 1, QtCore.Qt.AlignHCenter) self.mw.gridLayout.setColumnStretch(buttonIndex + 1, 10) button.clicked.connect(partial( self.__class__._onColumnButtonClicked, weakref.proxy( self), buttonIndex)) def _addRowButton(self, buttonTitles, buttonColors, inverseRawColumn, rowButtonSubTitles=None): self.rowsButtonGroup = QButtonGroup(self.mw) self.rowsButtonGroup.setExclusive(True) for buttonIndex in range(0, len(buttonTitles)): title = buttonTitles[buttonIndex] NotNoneCount = len( [x for x in self.anatomistObjectList[buttonIndex] if x != None]) isFusionPossibleOnRow = NotNoneCount > 1 or len( self._overlaid_images) > 0 widget = DisplayTitledGrid._createColoredButton( title, buttonColors[buttonIndex]) self.rowsButtonGroup.addButton(widget, buttonIndex) widget.setToolTip( '<p>Click on this button to superimpose a different image. To do so, click on this row button, then click on a column button to display the column main image as overlay on this row.<p><p>Click again on the tow button to go back to the initial views.</p>') widget.clicked.connect(partial( self.__class__._onRowButtonClicked, weakref.proxy( self), buttonIndex)) if(rowButtonSubTitles is not None and buttonIndex < len(rowButtonSubTitles)): subTitle = rowButtonSubTitles[buttonIndex] vLay = QVBoxLayout() vLay.insertStretch(0, 2) vLay.addWidget(widget) vLay.addWidget(QLabel(subTitle)) vLay.addStretch(2) if (inverseRawColumn): self.mw.gridLayout.addLayout(vLay, 0, buttonIndex + 1) self.mw.gridLayout.setColumnStretch(buttonIndex + 1, 10) else: self.mw.gridLayout.addLayout(vLay, buttonIndex + 1, 0) self.mw.gridLayout.setRowStretch(buttonIndex + 1, 10) else: if (inverseRawColumn): self.mw.gridLayout.addWidget(widget, 0, buttonIndex + 1) self.mw.gridLayout.setColumnStretch(buttonIndex + 1, 10) else: self.mw.gridLayout.addWidget(widget, buttonIndex + 1, 0) self.mw.gridLayout.setRowStretch(buttonIndex + 1, 10) @staticmethod def _createColoredButton(title, color): button = QRadioButton(title) buttonPalette = QPalette() buttonPalette.setColor(QPalette.ButtonText, Qt.QColor(color)) button.setPalette(buttonPalette) # button.setDisabled(True) button.setCheckable(True) return button def _createAndLinkAnatomistWindowsInMainLayout(self, linkWindows, inverseRawColumn, initialView, spaceNames): mw = self.mw mw.anaWinMatrix = [] for r in range(0, len(self.anatomistObjectList)): anaWinRow = self._createAnatomistWindows_InMainLayout( inverseRawColumn, initialView, r) DisplayTitledGrid._linkAnatomistWindows( linkWindows, anaWinRow, spaceNames) def _createAnatomistWindows_InMainLayout(self, inverseRawColumn, view, rowIndex): mw = self.mw a = ana.Anatomist() anaObjRow = self.anatomistObjectList[rowIndex] anaWinRow = [] anatomistConfig = a.config() isWindowSizeFactorExistInConfig = False if 'windowSizeFactor' in anatomistConfig: sizefac = a.config()['windowSizeFactor'] isWindowSizeFactorExistInConfig = True a.config()['windowSizeFactor'] = 1. for c in range(0, len(anaObjRow)): anaObj = anaObjRow[c] if (anaObj is not None): w = a.createWindow(view, no_decoration=True) anaObj.addInWindows([w]) anaWinRow.append(w) frame = self._createWinFrame(mw, w.getInternalRep()) if (inverseRawColumn): mw.gridLayout.addWidget(frame, c + 1, rowIndex + 1) else: mw.gridLayout.addWidget(frame, rowIndex + 1, c + 1) else: anaWinRow.append(None) if(isWindowSizeFactorExistInConfig): a.config()['windowSizeFactor'] = sizefac mw.anaWinMatrix.append(anaWinRow) return mw.anaWinMatrix def _createWinFrame(self, mw, widget): mw.frame = QFrame() mw.flay = QVBoxLayout(mw.frame) mw.flay.addWidget(widget) mw.frame.setObjectName('winborder') mw.frame.setStyleSheet( 'QFrame#winborder { border: 0px solid; border-radius: 4px; }') pal = mw.frame.palette() pal.setColor(QPalette.Dark, QColor(255, 192, 0)) pal.setColor(QPalette.Midlight, QColor(192, 255, 0)) pal.setColor(QPalette.Shadow, QColor(192, 0, 255)) pal.setColor(QPalette.Light, QColor(0, 255, 192)) pal.setColor(QPalette.Mid, QColor(0, 192, 255)) return mw.frame @staticmethod def _linkAnatomistWindows(linkWindows, anaWinRow, spaceNames): if (linkWindows == 'all'): DisplayTitledGrid._linkAnatomistWindows_all(anaWinRow) elif (linkWindows == 'row'): DisplayTitledGrid._linkAnatomistWindows_byRow(anaWinRow) elif (linkWindows == 'space'): DisplayTitledGrid._linkAnatomistWindows_bySpace( anaWinRow, spaceNames) @staticmethod def _linkAnatomistWindows_all(anaWinMatrix): a = ana.Anatomist() wins = [] for anaWinRow in anaWinMatrix: for w in anaWinRow: if (w is not None): wins.append(w) a.linkWindows(wins, group=None) a.execute('WindowConfig', windows=wins, linkedcursor_on_slider_change=1) @staticmethod def _linkAnatomistWindows_byRow(anaWinMatrix): a = ana.Anatomist() for anaWinRow in anaWinMatrix: wins = [] for w in anaWinRow: if (w is not None): wins.append(w) a.linkWindows(wins, group=None) a.execute('WindowConfig', windows=wins, linkedcursor_on_slider_change=1) @staticmethod def _linkAnatomistWindows_bySpace(anaWinMatrix, spaceNames): a = ana.Anatomist() winsDico = {} for anaWinRow, spaceName in zip(anaWinMatrix, spaceNames): isRawSpace = spaceName.lower().count('raw') != 0 if (isRawSpace is False): # inutile de lier les fenetres si leur images sont des raw, donc dans leur propre espace. Par exemple, ne pas lier la pet avec l'irm keySpace = DisplayTitledGrid._convertSpaceName_to_key( spaceName) for w in anaWinRow: if (w is not None): if keySpace in winsDico: prevWins = winsDico[keySpace] prevWins.append(w) winsDico.update({keySpace: prevWins}) else: winsDico.update({keySpace: [w]}) for _k, wins in winsDico.items(): a.linkWindows(wins, group=None) a.execute('WindowConfig', windows=wins, linkedcursor_on_slider_change=1) @staticmethod def _convertSpaceName_to_key(spaceName): isMRISpace = spaceName.lower().count('mri') > 0 isPETSpace = spaceName.lower().count('pet') > 0 isMNISpace = spaceName.lower().count('mni') > 0 keySpace = spaceName if (isMRISpace): keySpace = 'mri' elif (isPETSpace): keySpace = 'pet' elif (isMNISpace): keySpace = 'mni' return keySpace def _onComboBox_changed(self): for anaWinRow in self.mw.anaWinMatrix: for w in anaWinRow: if(w is not None): if(self.mw.comboBox.currentText() == 'Axial'): w.muteAxial() elif(self.mw.comboBox.currentText() == 'Sagittal'): w.muteSagittal() elif(self.mw.comboBox.currentText() == 'Coronal'): w.muteCoronal() def _loadOverlayImages(self, overlaidImages): a = ana.Anatomist() images = [] for filename in overlaidImages: if filename: # may be None to leave an un-overlayed row image = a.loadObject(filename, forceReload=False) images.append(image) image.setPalette(palette=self._overlay_colormap) else: # None images.append(None) self._overlaid_images = images def _createOverlayFusions(self): if len(self._overlaid_images) == 0: # no overlays, nothing to be done. return matriceFusions = [] if len(self._overlaid_images) == len(self.anatomistObjectList) \ * len(self.anatomistObjectList[0]): indiv_index = 0 for row, objRow in enumerate(self.anatomistObjectList): overlays = [] for col, objCol in enumerate(objRow): index = indiv_index indiv_index += 1 overlayimage = self._overlaid_images[index] overlays.append(overlayimage) rowFusions = self._createFusionsWithOverlay(objRow, overlays) matriceFusions.append(rowFusions) else: for row, objRow in enumerate(self.anatomistObjectList): index = row if index >= len(self._overlaid_images): overlayimage = self._overlaid_images[-1] else: overlayimage = self._overlaid_images[index] rowFusions = self._createFusionsWithOverlay( objRow, overlayimage) matriceFusions.append(rowFusions) self._overlay_fusions = matriceFusions def _createCustomOverlayFusions(self, row, column): if row >= 0 and column >= 0: overlayimage = self.anatomistObjectList[row][column] if overlayimage is not None: newoverlay = self._setPaletteOfOverlay(overlayimage) rowFusions = self._createFusionsWithOverlay( self.anatomistObjectList[row], newoverlay, overlayimage) if len(self._custom_overlay_fusions) <= row: self._custom_overlay_fusions.extend( [[]] * (row + 1 - len(self._custom_overlay_fusions))) self._custom_overlay_fusions[row] = rowFusions a = ana.Anatomist() a.execute('TexturingParams', objects=[ x for x in rowFusions if x], texture_index=1, rate=float(self.mw.mixingSlider.value()) / 100) elif(row < len(self._custom_overlay_fusions)): self._custom_overlay_fusions[row] = None def _createFusionsWithOverlay(self, objects, overlayimages, imageWithoutFusion=None): if not isinstance(overlayimages, list): overlayimages = [overlayimages] a = ana.Anatomist() rowFusions = [] for index, obj in enumerate(objects): if index < len(overlayimages): overlayimage = overlayimages[index] else: overlayimage = overlayimages[-1] if obj and overlayimage and obj != overlayimage and (not imageWithoutFusion or imageWithoutFusion != obj): fusion = a.fusionObjects( objects=[obj, overlayimage], method='Fusion2DMethod') rowFusions.append(fusion) else: rowFusions.append(None) return rowFusions def _setPaletteOfOverlay(self, overlayimage): a = ana.Anatomist() if (self._custom_overlay_colormap is not None): newoverlay = a.duplicateObject(overlayimage) newoverlay.setPalette(self._custom_overlay_colormap) else: newoverlay = overlayimage overlayimagepalette = overlayimage.palette().refPalette() paletteName = overlayimagepalette.name() newoverlay.setPalette(paletteName) return newoverlay def _addObjectOrFusion_inAnatomistWindows(self): if len(self._overlay_fusions) == len(self.mw.anaWinMatrix): byrow = False else: byrow = True indiv_index = 0 for row, _anaWinRow in enumerate(self.mw.anaWinMatrix): if byrow: index = row else: index = indiv_index indiv_index += 1 if index < len(self._overlay_fusions): fusRow = self._overlay_fusions[index] self._addObjectOrFusion_inAnatomistWindowsRow(row, fusRow) def _addObjectOrFusion_inAnatomistWindowsRow(self, rowIndex, rowFusions): # rowFusions can be self._overlay_fusions or self._custom_overlay_fusions if(rowIndex >= 0): anaWinRow = self.mw.anaWinMatrix[rowIndex] objRow = self.anatomistObjectList[rowIndex] for col, win in enumerate(anaWinRow): if win: if win.objects: win.removeObjects(win.objects) if rowFusions and rowFusions[col]: win.addObjects(rowFusions[col]) elif objRow and objRow[col]: win.addObjects(objRow[col]) def _removeCustomOverlays(self, row): self._custom_overlay_fusions[row] = [] self._addObjectOrFusion_inAnatomistWindowsRow( row, self._overlay_fusions[row]) def _onMixingRateChanged(self, value): self.mw.mixRate.setText(str(value) + ' %') a = ana.Anatomist() objects = [] for fusRow in self._overlay_fusions: if(fusRow): objects.extend([x for x in fusRow if x]) for fusRow in self._custom_overlay_fusions: if(fusRow): objects.extend([x for x in fusRow if x]) a.execute('TexturingParams', objects=objects, texture_index=1, rate=float(value) / 100) def _onColumnButtonClicked(self, column): oldcolumn = self._selectedColumn self._selectedColumn = column row = self.rowsButtonGroup.checkedId() self._removeWinFrame(row, oldcolumn) self._createCustomOverlayFusions(row, column) if(0 <= row and row < len(self._custom_overlay_fusions)): self._addObjectOrFusion_inAnatomistWindowsRow( row, self._custom_overlay_fusions[row]) self._highlightWinFrame(row, column) self._updatePalette() self._updateSelectedReferenceName() def _onRowButtonClicked(self, row): self._createCustomOverlayFusions(row, self._selectedColumn) self._addObjectOrFusion_inAnatomistWindowsRow(self._selectedRow, self._selectRowForFusions( self._selectedRow, thisRowIsSelected=False)) # reset previous selectedRow self._removeWinFrame(self._selectedRow, self._selectedColumn) isRowUnselected = self._selectedRow == row if (isRowUnselected): self._unselectRowForFusion(row) else: self._addObjectOrFusion_inAnatomistWindowsRow( row, self._selectRowForFusions(row)) self._highlightWinFrame(row, self._selectedColumn) self._updatePalette() self._updateSelectedReferenceName() def _removeWinFrame(self, row, column): if row >= 0 and row < len(self.mw.anaWinMatrix) and column >= 0: winrow = self.mw.anaWinMatrix[row] if column < len(winrow): anatomistWindow = winrow[column] if(anatomistWindow is not None): anatomistWindow.parent().setStyleSheet( 'QFrame#winborder { border: 0px; }') # momoTODO : il n'y a pas de parent! def _highlightWinFrame(self, row, column): if row >= 0 and row < len(self.mw.anaWinMatrix) and column >= 0: winrow = self.mw.anaWinMatrix[row] if column < len(winrow): if(winrow[column] is not None): winrow[column].parent().setStyleSheet( 'QFrame#winborder { border: 2px solid #c06000; border-radius: 4px; }') def _updatePalette(self): if (self._selectedColumn >= 0 and self._selectedRow >= 0): if (self._paletteEditor is not None): self._paletteEditor.close() selectedImage = self.anatomistObjectList[ self._selectedRow][self._selectedColumn] if(selectedImage is not None): self._paletteEditor = PaletteEditor( selectedImage, parent=self.mw, real_max=10000, sliderPrecision=10000, zoom=1) self.mw.horizontalLayout.insertWidget(3, self._paletteEditor) def _updateSelectedReferenceName(self): if (self._selectedColumn >= 0 and self._selectedRow >= 0): self.mw.selectedReferenceName.setText('<b><font color=#c06000>' + self._col_titles[ self._selectedColumn] + '_' + self._row_titles[self._selectedRow] + '</font></b>') self.mw.selectedReferenceName.setStyleSheet( '#selectedReferenceName { border: 2px solid #c06000; border-radius: 4px; padding: 4px; }') else: self.mw.selectedReferenceName.setText('None') self.mw.selectedReferenceName.setStyleSheet( '#selectedReferenceName { border: 0px; }') def _unselectRowForFusion(self, row): self._selectedRow = -1 self._unselectButtonInGroup(self.rowsButtonGroup, row) # button.setText(self._row_titles[self._selectedRow])# momoTODO : pas # besoin de changer le text si c'est un radio bouton. Le text peut # contenir une information d'espace (mni, mri...) à ne pas mélanger avec # la fusion def _unselectButtonInGroup(self, group, buttonId): if(buttonId >= 0): button = group.button(buttonId) group.setExclusive(False) button.setChecked(False) group.setExclusive(True) def _selectRowForFusions(self, row, thisRowIsSelected=True): self._selectedRow = row fusions = None isCustomFusionsExist = len(self._custom_overlay_fusions) > 0 and len( self._custom_overlay_fusions) > self._selectedRow isFusionsExist = len(self._overlay_fusions) > 0 and len( self._overlay_fusions) > self._selectedRow if isCustomFusionsExist and thisRowIsSelected: fusions = self._custom_overlay_fusions[self._selectedRow] elif isFusionsExist: fusions = self._overlay_fusions[self._selectedRow] return fusions def _onMaximizeButtonClicked(self): print("_onMaximizeButtonClicked")
# momoTODO utiliser display fusion et afficher la fusion de tous # les objets de la ligne sélectionnée # momoTODO : encadrer la reference utiliser pour la fusion # painter = QPainter(mw) # painter.setPen(Qt.QColor('yellow')) # cellRect = mw.gridLayout.cellRect (rowIndex + 1, c + 1 ) # cellRectWidth = cellRect.width() # cellRect.setWidth(cellRectWidth+200) # painter.fillRect(cellRect, Qt.QColor('yellow')) # painter.drawRect(cellRect)