VTK+PyQt5: Connecting slider values to translation variables - python

I used the code below and tried to change slider value with first variable of transform.trasnlate(x,y,z)
but I faced the error below:
TypeError: Translate argument %Id: %V
details: I want to change the position of STL file as the the slider moves up or down.
I think my problem is that I'm using a method instead of an integer number, but I don't know how to fix. it
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.widget = QtWidgets.QWidget()
self.gridLayout = QtWidgets.QGridLayout()
#First STL file
reader = vtk.vtkSTLReader()
reader.SetFileName(filename)
transform = vtk.vtkTransform()
transform.Translate(self.size, 0, 0)
transformFilter = vtk.vtkTransformPolyDataFilter()
transformFilter.SetTransform(transform)
transformFilter.SetInputConnection(reader.GetOutputPort())
transformFilter.Update()
mapper = vtk.vtkPolyDataMapper()
if vtk.VTK_MAJOR_VERSION <= 5:
mapper.SetInput(transformFilter.GetOutput())
else:
mapper.SetInputConnection(transformFilter.GetOutputPort())
actor = vtk.vtkActor()
actor.SetMapper(mapper)
self.vtkwidget = QVTKRenderWindowInteractor(self.widget)
self.gridLayout.addWidget(self.vtkwidget, 1, 0, 1, 1)
#Slider
self.title = "Rotaion"
self.top = 200
self.left = 500
self.width = 400
self.height = 300
self.setWindowIcon(QtGui.QIcon("icon.png"))
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
self.slider = QSlider()
self.slider.setOrientation(Qt.Vertical)
self.slider.setTickPosition(QSlider.TicksBelow)
self.slider.setTickInterval(10)
self.slider.setMinimum(-100)
self.slider.setMaximum(100)
self.slider.valueChanged.connect(self.handler)
self.gridLayout.addWidget(self.slider, 1, 1, 1, 1)
# Create a rendering window and renderer
self.ren = vtk.vtkRenderer()
self.vtkwidget.GetRenderWindow().AddRenderer(self.ren)
self.iren = self.vtkwidget.GetRenderWindow().GetInteractor()
self.ren.AddActor(actor)
self.iren.Initialize()
# Enable user interface interactor
self.widget.setLayout(self.gridLayout)
self.setCentralWidget(self.widget)
self.ren.ResetCamera()
def handler(self):
global size
self.size = self.slider.value()
print(self.size)
#self.label.setText(str(self.size))
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())

size() is one of the basic default properties of all QWidget classes, and since all Qt properties are accessible as callables what you're getting with self.size (without parentheses) is a reference to a method, and that's clearly not an acceptable type for the transform. In any case, the self.size() will return a QSize value, which is also not an acceptable type.
So, two considerations must be done:
No existing attribute of the inherited class(es) should ever be overwritten for uses different from their scope; since you're using a QMainWindow, you'll need to ensure that you're not overwriting any of the properties or functions of QMainWindow, QWidget and QObject; I strongly recommend you to always check for them (you can see the full member list by clicking the "List of all members, including inherited members" link at the beginning of the documentation of each class), but the rule of thumb is: if it's a very generic attribute name (like "size", "width", "font") it probably exists already and should not be overwritten.
If size wasn't an already existing attribute, you'd have faced an AttributeError, because you never declared it before that point.
Finally, if you want to change the transform, you certainly cannot just do that by overwriting the self.size value; Python doesn't work like that, at least with simple instance attributes: you'll have only changed the value of self.size, but the transform object can know anything about that.
You need to connect the valueChanged signal to a slot that changes the transform (or sets a new one). I don't use Vtk, so the following is just some pseudo code:
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
# ...
self.transformSize = 0
self.transform = vtk.vtkTransform()
self.transform.Translate(self.transformSize, 0, 0)
self.transformFilter = vtk.vtkTransformPolyDataFilter()
# ...
def handler(self, value):
self.transformSize = value
self.transform.Translate(value, 0, 0)
self.transformFilter.Update()
Some considerations:
As with size(), you should also not overwrite other existing attributes like width() or height().
Almost every signal that is emitted when some property has changed, provides the new value as argument, and that's also true for QAbstractSlider's valueChanged, so you don't need to use self.slider.value() inside handler() since that value is already the argument.
Grid layouts (as any other index-based object) are 0-index based, if you don't add anything to the first row or column of the grid (or you don't set a minimum height/width for them) there's no use adding widgets to the second row/column; also if the added widget is only using one "grid cell", the rowSpan and columnSpan arguments (the last 1, 1 in addWidget) are superfluous since they are the default values.
I don't know what that global in the handler() is doing there, since it's not used, but keep in mind that you should always avoid globals: the general rule is that "if you're using them, you're doing something wrong", or, better, "you should not use globals unless you know when you need them, and if you really know when you need them, you also know that you should not use them".

Related

PyQt5 left click not working for mouseMoveEvent

I'm trying to learn PyQt5, and I've got this code:
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.label = QLabel()
canvas = QPixmap(400, 300)
canvas.fill(Qt.white)
self.label.setPixmap(canvas)
self.setCentralWidget(self.label)
def mouseMoveEvent(self, e):
painter = QPainter(self.label.pixmap())
painter.drawPoint(e.x(), e.y())
painter.end()
self.update()
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
And I can draw using right click to draw, but when I left click, it drags the window instead of drawing. This even happens when I make the window fullscreen so I can't move it. How can I stop it from dragging the window so it will draw instead?
In some configurations (specifically, on Linux, and depending on the window manager settings), dragging the left mouse button on an empty (non interactive) area of a QMainWindow allows dragging the whole window.
To prevent that, the mouse move event has to be accepted by the child widget that receives it.
While this can be achieved with an event filter, it's usually better to use a subclass, and this is even more important whenever the widget has to deal with mouse events it receives, exactly like in this case.
Another aspect that has to be considered is that just updating the QLabel pixmap is not completely sufficient, because it doesn't automatically force its update. Also, since Qt 5.15, QLabel.pixmap() doesn't return a pointer to the pixmap, but rather its copy. This means that you should always keep a local reference to the pixmap for the whole time required to access it (otherwise your program will crash), and then call setPixmap() again with the updated pixmap after "ending" the painter. This will automatically schedule an update of the label.
The above may be a bit confusing if you're not used to languages that allow pointers as arguments, but, in order to clarify how it works, you can consider the pixmap() property similarly to the text() one:
text = self.label.text()
text += 'some other text'
The above will obviously not change the text of the label, most importantly because, in most languages (including Python) strings are always immutable objects, so text += ... actually replaces the text reference with another string object.
To clarify, consider the following:
text1 = text2 = self.label.text()
text1 += 'some other text'
print(text1 == text2)
Which will return False.
Now consider this instead:
list1 = list2 = []
list1 += ['item']
print(list1 == list2)
Which will return True, because list is a mutable type, and in python changing the content of a mutable type will affect any reference to it[1], since they refer to the same object.
Until Qt < 5.15, the pixmap of QLabel behaved similarly to a list, meaning that any painting on the label.pixmap() would actually change the content of the displayed pixmap (requiring label.update() to actually show the change). After Qt 5.15 this is no longer valid, as the returned pixmap behaves similarly to a returned string: altering its contents won't change the label's pixmap.
So, the proper way to update the pixmap is to:
handle the mouse event in the label instance (either by subclassing or using an event filter), and not in a parent;
get the pixmap, keep its reference until painting has completed, and call setPixmap() afterwards (mandatory since Qt 5.15, but also suggested anyway);
Finally, QLabel has an alignment property that, when using a pixmap, is used to set the alignment of the pixmap to the available space that the layout manager provides. The default is left aligned and vertically centered (Qt.AlignLeft|Qt.AlignVCenter).
QLabel also features the scaledContents property, which always scales the pixmap to the current size of the label (not considering the aspect ratio).
The above means one of the following:
the pixmap will always be displayed at its actual size, and eventually aligned within its available space;
if the scaledContents property is True, the alignment is ignored and the pixmap will be always scaled to the full extent of its available space; whenever that property is True, the resulting pixmap is also cached, so you have to clear its cache every time (at least, with Qt5);
if you need to always keep aspect ratio, using QLabel is probably pointless, and you may prefer a plain QWidget that actively draws the pixmap within a paintEvent() override;
Considering the above, here is a possible implementation of the label (ignoring the ratio):
class PaintLabel(QLabel):
def mouseMoveEvent(self, event):
pixmap = self.pixmap()
if pixmap is None:
return
pmSize = pixmap.size()
if not pmSize.isValid():
return
pos = event.pos()
scaled = self.hasScaledContents()
if scaled:
# scale the mouse position to the actual pixmap size
pos = QPoint(
round(pos.x() * pmSize.width() / self.width()),
round(pos.y() * pmSize.height() / self.height())
)
else:
# translate the mouse position depending on the alignment
alignment = self.alignment()
dx = dy = 0
if alignment & Qt.AlignRight:
dx += pmSize.width() - self.width()
elif alignment & Qt.AlignHCenter:
dx += round((pmSize.width() - self.width()) / 2)
if alignment & Qt.AlignBottom:
dy += pmSize.height() - self.height()
elif alignment & Qt.AlignVCenter:
dy += round((pmSize.height() - self.height()) // 2)
pos += QPoint(dx, dy)
painter = QPainter(pixmap)
painter.drawPoint(pos)
painter.end()
# this will also force a scheduled update
self.setPixmap(pixmap)
if scaled:
# force pixmap cache clearing
self.setScaledContents(False)
self.setScaledContents(True)
def minimumSizeHint(self):
# just for example purposes
return QSize(10, 10)
And here is an example of its usage:
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.label = PaintLabel()
canvas = QPixmap(400, 300)
canvas.fill(Qt.white)
self.label.setPixmap(canvas)
self.hCombo = QComboBox()
for i, hPos in enumerate(('Left', 'HCenter', 'Right')):
hAlign = getattr(Qt, 'Align' + hPos)
self.hCombo.addItem(hPos, hAlign)
if self.label.alignment() & hAlign:
self.hCombo.setCurrentIndex(i)
self.vCombo = QComboBox()
for i, vPos in enumerate(('Top', 'VCenter', 'Bottom')):
vAlign = getattr(Qt, 'Align' + vPos)
self.vCombo.addItem(vPos, vAlign)
if self.label.alignment() & vAlign:
self.vCombo.setCurrentIndex(i)
self.scaledChk = QCheckBox('Scaled')
central = QWidget()
mainLayout = QVBoxLayout(central)
panel = QHBoxLayout()
mainLayout.addLayout(panel)
panel.addWidget(self.hCombo)
panel.addWidget(self.vCombo)
panel.addWidget(self.scaledChk)
mainLayout.addWidget(self.label)
self.setCentralWidget(central)
self.hCombo.currentIndexChanged.connect(self.updateLabel)
self.vCombo.currentIndexChanged.connect(self.updateLabel)
self.scaledChk.toggled.connect(self.updateLabel)
def updateLabel(self):
self.label.setAlignment(Qt.AlignmentFlag(
self.hCombo.currentData() | self.vCombo.currentData()
))
self.label.setScaledContents(self.scaledChk.isChecked())
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
Note that if you need more advanced control over the pixmap display and painting (including aspect ratio, but also proper zoom capabilities and any possible complex feature), then the common suggestion is to completely ignore QLabel, as said above: either use a basic QWidget, or consider the more complex (but much more powerful) Graphics View Framework. This will also allow proper editing features, as you can add non-destructive editing that will show ("paint") the result without affecting the actual, original object.
[1]: The above is based on the fact that a function or operator can actually mutate the object: the += operator actually calls the __add__ magic method that, in the case of lists, updates the contents of the same list.

How to save selected element from combobox as variable in PyQt5?

I'm interested in how to save a selected value from my combobox as variable, so when I press e.g. B then I want it to be saved as SelectedValueCBox = selected value, which would be B in this case.
Thank you for your help
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
import sys
class App(QMainWindow):
def __init__(self):
super().__init__()
self.title = "PyQt5 - StockWindow"
self.left = 0
self.top = 0
self.width = 200
self.height = 300
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
self.tab_widget = MyTabWidget(self)
self.setCentralWidget(self.tab_widget)
self.show()
class MyTabWidget(QWidget):
def __init__(self, parent):
super(QWidget, self).__init__(parent)
self.layout = QVBoxLayout(self)
#self.layout = QGridLayout(self)
self.tabs = QTabWidget()
self.tab1 = QWidget()
self.tabs.resize(300, 200)
self.tabs.addTab(self.tab1, "Stock-Picker")
self.tab1.layout = QGridLayout(self)
button = QToolButton()
self.tab1.layout.addWidget(button, 1,1,1,1)
d = {'AEX':['A','B','C'], 'ATX':['D','E','F'], 'BEL20':['G','H','I'], 'BIST100':['J','K','L']}
def callback_factory(k, v):
return lambda: button.setText('{0}_{1}'.format(k, v))
menu = QMenu()
self.tab1.layout.addWidget(menu, 1,1,1,1)
for k, vals in d.items():
sub_menu = menu.addMenu(k)
for v in vals:
action = sub_menu.addAction(str(v))
action.triggered.connect(callback_factory(k, v))
button.setMenu(menu)
self.tab1.setLayout(self.tab1.layout)
self.layout.addWidget(self.tabs)
self.setLayout(self.layout)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = App()
sys.exit(app.exec_())
Since you're already returning a lambda for the connection, the solution is to use a function instead.
class MyTabWidget(QWidget):
def __init__(self, parent):
# ...
def callback_factory(k, v):
def func():
self.selectedValueCBox = v
button.setText('{0}_{1}'.format(k, v))
return func
# ...
self.selectedValueCBox = None
Note that your code also has many issues.
First of all, you should not add the menu to the layout: not only it doesn't make any sense (the menu should pop up, while adding it to a layout makes it "embed" into the widget, and that's not good), but it also creates graphical issues especially because you added the menu to the same grid "slot" (1, 1, 1, 1) which is already occupied by the button.
Creating a layout with a widget as argument automatically sets the layout to that widget. While in your case it doesn't create a big issue (since you've already set the layout) you should not create self.tab1.layout with self. Also, since you've already set the QVBoxLayout (due to the parent argument), there's no need to call setLayout() again.
A widget container makes sense if you're actually going to add more than one widget. You're only adding a QTabWidget to its layout, so it's almost useless, and you should just subclass from QTabWidget instead.
Calling resize on a widget that is going to be added on a layout is useless, as the layout will take care of the resizing and the previous resize call will be completely ignored. resize() makes only sense for top level widgets (windows) or the rare case of widgets not managed by a layout.
self.layout() is an existing property of all QWidgets, you should not overwrite it. The same with self.width() and self.height() you used in the App class.
App should refer to an application class, but you're using it for a QMainWindow. They are radically different types of classes.
Finally, you have no combobox in your code. A combobox is widget that is completely different from a drop down menu like the one you're using. I suggest you to be more careful with the terminology in the future, otherwise your question would result very confusing, preventing people to actually being able to help you.

How to word wrap the header contents of QTableWidget in PyQt5 Python

I am working on PyQt5 where I have a QTableWidget. It has a header column which I want to word wrap. Below is how the table looks like:
As we can see that the header label like Maximum Variation Coefficient has 3 words, thus its taking too much column width. How can wrap the words in the header.
Below is the code:
import sys
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import *
# Main Window
class App(QWidget):
def __init__(self):
super().__init__()
self.title = 'PyQt5 - QTableWidget'
self.left = 0
self.top = 0
self.width = 300
self.height = 200
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
self.createTable()
self.layout = QVBoxLayout()
self.layout.addWidget(self.tableWidget)
self.setLayout(self.layout)
# Show window
self.show()
# Create table
def createTable(self):
self.tableWidget = QTableWidget()
# Row count
self.tableWidget.setRowCount(3)
# Column count
self.tableWidget.setColumnCount(2)
self.tableWidget.setHorizontalHeaderLabels(["Maximum Variation Coefficient", "Maximum Variation Coefficient"])
self.tableWidget.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
self.tableWidget.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
self.tableWidget.setItem(0, 0, QTableWidgetItem("3.44"))
self.tableWidget.setItem(0, 1, QTableWidgetItem("5.3"))
self.tableWidget.setItem(1, 0, QTableWidgetItem("4.6"))
self.tableWidget.setItem(1, 1, QTableWidgetItem("1.2"))
self.tableWidget.setItem(2, 0, QTableWidgetItem("2.2"))
self.tableWidget.setItem(2, 1, QTableWidgetItem("4.4"))
# Table will fit the screen horizontally
self.tableWidget.horizontalHeader().setStretchLastSection(True)
self.tableWidget.horizontalHeader().setSectionResizeMode(
QHeaderView.Stretch)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = App()
sys.exit(app.exec_())
I tried adding this self.tableWidget.setWordWrap(True) but this doesnt make any change. Can anyone give some good solution. Please help. Thanks
EDIT:
Also tried this :
self.tableWidget.horizontalHeader().setDefaultAlignment(QtCore.Qt.AlignHCenter | Qt.Alignment(QtCore.Qt.TextWordWrap))
But it didnt worked
In order to achieve what you need, you must set your own header and proceed with the following two assumptions:
the header must provide the correct size hint height according to the section contents in case the width of the column is not sufficient;
the text alignment must include the QtCore.Qt.TextWordWrap flag, so that the painter knows that it can wrap text;
Do note that, while the second aspect might be enough in some situations (as headers are normally tall enough to fit at least two lines), the first point is mandatory as the text might require more vertical space, otherwise some text would be cut out.
The first point requires to subclass QHeaderView and reimplement sectionSizeFromContents():
class WrapHeader(QtWidgets.QHeaderView):
def sectionSizeFromContents(self, logicalIndex):
# get the size returned by the default implementation
size = super().sectionSizeFromContents(logicalIndex)
if self.model():
if size.width() > self.sectionSize(logicalIndex):
text = self.model().headerData(logicalIndex,
self.orientation(), QtCore.Qt.DisplayRole)
if not text:
return size
# in case the display role is numeric (for example, when header
# labels are not defined yet), convert it to a string;
text = str(text)
option = QtWidgets.QStyleOptionHeader()
self.initStyleOption(option)
alignment = self.model().headerData(logicalIndex,
self.orientation(), QtCore.Qt.TextAlignmentRole)
if alignment is None:
alignment = option.textAlignment
# get the default style margin for header text and create a
# possible rectangle using the current section size, then use
# QFontMetrics to get the required rectangle for the wrapped text
margin = self.style().pixelMetric(
QtWidgets.QStyle.PM_HeaderMargin, option, self)
maxWidth = self.sectionSize(logicalIndex) - margin * 2
rect = option.fontMetrics.boundingRect(
QtCore.QRect(0, 0, maxWidth, 10000),
alignment | QtCore.Qt.TextWordWrap,
text)
# add vertical margins to the resulting height
height = rect.height() + margin * 2
if height >= size.height():
# if the height is bigger than the one provided by the base
# implementation, return a new size based on the text rect
return QtCore.QSize(rect.width(), height)
return size
class App(QWidget):
# ...
def createTable(self):
self.tableWidget = QTableWidget()
self.tableWidget.setHorizontalHeader(
WrapHeader(QtCore.Qt.Horizontal, self.tableWidget))
# ...
Then, to set the word wrap flag, there are two options:
set the alignment flag on the underlying model with setHeaderData() for each existing column:
# ...
model = self.tableWidget.model()
default = self.tableWidget.horizontalHeader().defaultAlignment()
default |= QtCore.Qt.TextWordWrap
for col in range(self.tableWidget.columnCount()):
alignment = model.headerData(
col, QtCore.Qt.Horizontal, QtCore.Qt.TextAlignmentRole)
if alignment:
alignment |= QtCore.Qt.TextWordWrap
else:
alignment = default
model.setHeaderData(
col, QtCore.Qt.Horizontal, alignment, QtCore.Qt.TextAlignmentRole)
Use a QProxyStyle to override the painting of the header, by applying the flag on the option:
# ...
class ProxyStyle(QtWidgets.QProxyStyle):
def drawControl(self, control, option, painter, widget=None):
if control in (self.CE_Header, self.CE_HeaderLabel):
option.textAlignment |= QtCore.Qt.TextWordWrap
super().drawControl(control, option, painter, widget)
if __name__ == '__main__':
app = QApplication(sys.argv)
app.setStyle(ProxyStyle())
ex = App()
sys.exit(app.exec_())
Finally, consider that:
using setSectionResizeMode with ResizeToContents or Stretch, along with setStretchLastSection, will always cause the table trying to use as much space as required by the headers upon showing the first time;
by default, QHeaderView sections are not clickable (which is a mandatory requirement for sorting) and the highlightSections property is also False; both QTableView and QTableWidget create their headers with those values as True, so when a new header is set you must explicitly change those aspects if sorting and highlighting are required:
self.tableWidget.setHorizontalHeader(
WrapHeader(QtCore.Qt.Horizontal, self.tableWidget))
self.tableWidget.horizontalHeader().setSectionsClickable(True)
self.tableWidget.horizontalHeader().setHighlightSections(True)
both sorting and section highlighting can create some issues, as the sort indicator requires further horizontal space and highlighted sections are normally shown with a bold font (but are shown normally while the mouse is pressed); all this might create some flickering and odd behavior; unfortunately, there's no obvious solution for these problems, but when using the QProxyStyle it's possible to avoid some flickering by overriding the font style:
def drawControl(self, control, option, painter, widget=None):
if control in (self.CE_Header, self.CE_HeaderLabel):
option.textAlignment |= QtCore.Qt.TextWordWrap
if option.state & self.State_Sunken:
option.state |= self.State_On
super().drawControl(control, option, painter, widget)

How to set selecting slider for a QTableWidget?

I'm looking for solution of merge discrete slider and QTableWidget (see attached screenshot). Slider is used as selecting pointer(instead of default selecting highlighter). How it can be implemented using Qt (PyQt)?
Small premise. Technically, according to StackOverflow standards, your question is not a very good one. I'll explain it at the end of this answer.
Getting what you are asking for is not easy, most importantly because sliders are not built for that purpose (and there are many UX reasons for which you should not do that, go to User Experience to ask about them).
The trick is to create a QSlider that has the table widget as a parent. Creating a widget with a parent ensures that the child widget will always be enclosed within the parent boundaries (this is only false for QMainWindow and QDialog descendants), as long as the widget is not added to the parent layout. This allows you to freely set its geometry (position and size).
In the following example I'm adding an internal QSlider, but the main issue about this widget is aligning it in such a way that its value positions are aligned with the table contents.
class GhostHeader(QtWidgets.QHeaderView):
'''
A "fake" vertical header that does not paint its sections
'''
def __init__(self, parent):
super().__init__(QtCore.Qt.Vertical, parent)
self.setSectionResizeMode(self.Fixed)
def paintEvent(self, event):
pass
class SliderTable(QtWidgets.QTableWidget):
def __init__(self, rows=0, columns=0, parent=None):
super().__init__(rows, columns, parent)
self.horizontalHeader().setStretchLastSection(True)
self.setHorizontalHeaderLabels(['Item table'])
self.setVerticalHeader(GhostHeader(self))
# create a slider that is a child of the table; there is no layout, but
# setting the table as its parent will cause it to be shown "within" it.
self.slider = QtWidgets.QSlider(QtCore.Qt.Vertical, self)
# by default, a slider has its maximum on the top, let's invert this
self.slider.setInvertedAppearance(True)
self.slider.setInvertedControls(True)
# show tick marks at each slider value, on both sides
self.slider.setTickInterval(1)
self.slider.setTickPosition(self.slider.TicksBothSides)
self.slider.setRange(0, max(0, self.rowCount() - 1))
# not necessary, but useful for wheel and click interaction
self.slider.setPageStep(1)
# disable focus on the slider
self.slider.setFocusPolicy(QtCore.Qt.NoFocus)
self.slider.valueChanged.connect(self.selectRowFromSlider)
self.slider.valueChanged.connect(self.updateSlider)
self.verticalScrollBar().valueChanged.connect(self.updateSlider)
self.model().rowsInserted.connect(self.modelChanged)
self.model().rowsRemoved.connect(self.modelChanged)
def selectRowFromSlider(self, row):
if self.currentIndex().isValid():
column = self.currentIndex().column()
else:
column = 0
self.setCurrentIndex(self.model().index(row, column))
def modelChanged(self):
self.slider.setMaximum(max(0, self.rowCount() - 1))
self.updateSlider()
def updateSlider(self):
slider = self.slider
option = QtWidgets.QStyleOptionSlider()
slider.initStyleOption(option)
style = slider.style()
# get the available extent of the slider
available = style.pixelMetric(style.PM_SliderSpaceAvailable, option, slider)
# compute the space between the top of the slider and the position of
# the minimum value (0)
deltaTop = (slider.height() - available) // 2
# do the same for the maximum
deltaBottom = slider.height() - available - deltaTop
# the vertical center of the first item
top = self.visualRect(self.model().index(0, 0)).center().y()
# the vertical center of the last
bottom = self.visualRect(self.model().index(self.model().rowCount() - 1, 0)).y()
# get the slider width and adjust the size of the "ghost" vertical header
width = self.slider.sizeHint().width()
left = self.frameWidth() + 1
self.verticalHeader().setFixedWidth(width // 2 + left)
viewGeo = self.viewport().geometry()
headerHeight = viewGeo.top()
# create the rectangle for the slider geometry
rect = QtCore.QRect(0, headerHeight + top, width, headerHeight + bottom - top // 2)
# adjust to the values computed above
rect.adjust(0, -deltaTop + 1, 0, -deltaBottom)
# translate it so that its center will be between the vertical header and
# the table contents
rect.translate(left, 0)
self.slider.setGeometry(rect)
# set the mask, in case the item view is scrolled, so that the top of the
# slider won't be shown in the horizontal header
visible = self.rect().adjusted(0, viewGeo.top(), 0, 0)
mask = QtGui.QPainterPath()
topLeft = slider.mapFromParent(visible.topLeft())
bottomRight = slider.mapFromParent(visible.bottomRight() + QtCore.QPoint(1, 1))
mask.addRect(QtCore.QRectF(topLeft, bottomRight))
self.slider.setMask(QtGui.QRegion(mask.toFillPolygon(QtGui.QTransform()).toPolygon()))
def currentChanged(self, current, previous):
super().currentChanged(current, previous)
if current.isValid():
self.slider.setValue(current.row())
def resizeEvent(self, event):
# whenever the table is resized (even when first shown) call the base
# implementation (which is required for correct drawing of items and
# selections), then update the slider
super().resizeEvent(event)
self.updateSlider()
class Test(QtWidgets.QWidget):
def __init__(self):
super().__init__()
layout = QtWidgets.QVBoxLayout(self)
self.table = SliderTable()
self.table.setRowCount(4)
self.table.setColumnCount(1)
self.table.setHorizontalHeaderLabels(['Item table'])
layout.addWidget(self.table)
for row in range(self.table.rowCount()):
item = QtWidgets.QTableWidgetItem('item {}'.format(row + 1))
item.setTextAlignment(QtCore.Qt.AlignCenter)
self.table.setItem(row, 0, item)
Why this question is not that good?
Well, it's dangerously close to the "I don't know how to do this, can you do it for me?" limit. You should provide any minimal, reproducible example (it doesn't matter if it doesn't work, you should do some research and show your efforts), and the question is a bit vague, even after some clarifications in the comment sections.
Long story short: if it's too hard and you can't get it working, you probably still need some studying and exercise before you can achieve it. Be patient, study the documentation: luckily, Qt docs are usually well written, so it's just a matter of time.

QT, Python, QTreeView, custom widget, setData - lose reference after drag and drop

This question is similar to the one in this this topic Preserve QStandardItem subclasses in drag and drop but with issue that I cant find a good solution for. That topic partially helps but fail on more complex task.
When I create an item in QTreeView I put that item in my array but when I use drag&Drop the item gets deleted and I no longer have access to it. I know that its because drag and drop copies the item and not moves it so I should use setData. I cant setData to be an object because even then the object gets copied and I lose reference to it.
Here is an example
itemsArray = self.addNewRow
def addNewRow(self)
'''some code with more items'''
itemHolder = QStandardItem("ProgressBarItem")
widget = QProgressBar()
itemHolder.setData(widget)
inx = self.model.rowCount()
self.model.setItem(inx, 0, itemIcon)
self.model.setItem(inx, 1, itemName)
self.model.setItem(inx, 2, itemHolder)
ix = self.model.index(inx,2,QModelIndex())
self.treeView.setIndexWidget(ix, widget)
return [itemHolder, itemA, itemB, itemC]
#Simplified functionality
data = [xxx,xxx,xxx]
for items in itemsArray:
items[0].data().setPercentage(data[0])
items[1].data().setText(data[1])
items[2].data().setChecked(data[2])
The code above works if I won't move the widget. The second I drag/drop I lose reference I lose updates on all my items and I get crash.
RuntimeError: wrapped C/C++ object of type QProgressBar has been deleted
The way I can think of of fixing this problem is to loop over entire treeview recursively over each row/child and on name match update item.... Problem is that I will be refreshing treeview every 0.5 second and have 500+ rows with 5-15 items each. Meaning... I don't think that will be very fast/efficient... if I want to loop over 5 000 items every 0.5 second...
Can some one suggest how I could solve this problem? Perhaps I can edit dropEvent so it does not copy/paste item but rather move item.... This way I would not lose my object in array
Qt can only serialize objects that can be stored in a QVariant, so it's no surprise that this won't work with a QWidget. But even if it could serialize widgets, I still don't think it would work, because index-widgets belong to the view, not the model.
Anyway, I think you will have to keep references to the widgets separately, and only store a simple key in the model items. Then once the items are dropped, you can retrieve the widgets and reset them in the view.
Here's a working demo script:
from PyQt4 import QtCore, QtGui
class TreeView(QtGui.QTreeView):
def __init__(self, *args, **kwargs):
super(TreeView, self).__init__(*args, **kwargs)
self.setDragDropMode(QtGui.QAbstractItemView.InternalMove)
self.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)
self.setAllColumnsShowFocus(True)
self.setModel(QtGui.QStandardItemModel(self))
self._widgets = {}
self._dropping = False
self._droprange = range(0)
def dropEvent(self, event):
self._dropping = True
super(TreeView, self).dropEvent(event)
for row in self._droprange:
item = self.model().item(row, 2)
self.setIndexWidget(item.index(), self._widgets[item.data()])
self._droprange = range(0)
self._dropping = False
def rowsInserted(self, parent, start, end):
super(TreeView, self).rowsInserted(parent, start, end)
if self._dropping:
self._droprange = range(start, end + 1)
def addNewRow(self, name):
model = self.model()
itemIcon = QtGui.QStandardItem()
pixmap = QtGui.QPixmap(16, 16)
pixmap.fill(QtGui.QColor(name))
itemIcon.setIcon(QtGui.QIcon(pixmap))
itemName = QtGui.QStandardItem(name.title())
itemHolder = QtGui.QStandardItem('ProgressBarItem')
widget = QtGui.QProgressBar()
widget.setValue(5 * (model.rowCount() + 1))
key = id(widget)
self._widgets[key] = widget
itemHolder.setData(key)
model.appendRow([itemIcon, itemName, itemHolder])
self.setIndexWidget(model.indexFromItem(itemHolder), widget)
class Window(QtGui.QWidget):
def __init__(self):
super(Window, self).__init__()
self.treeView = TreeView()
for name in 'red yellow green purple blue orange'.split():
self.treeView.addNewRow(name)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.treeView)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.setGeometry(500, 150, 600, 400)
window.show()
sys.exit(app.exec_())

Categories

Resources