I have a QTableWidget in a window. Currently, the table's scrollbar goes all the way up to the top of the table's headers:
However, I would like the scrollbar to start below the headers, basically moving it down in the y axis.
I need it to start at the position of the scroll bar in the image below:
Does anyone have any idea how I could do this?
The simplest solution is to set the geometry of the scroll bar whenever the table resizes:
class Table(QtWidgets.QTableWidget):
# ...
def resizeEvent(self, event):
super().resizeEvent(event)
self.verticalScrollBar().setGeometry(
bar.geometry().adjusted(
0, self.horizontalHeader().height(), 0, 0))
Another similar possibility is to use a scroll bar widget added on top of the scroll bar, but the concept remains almost the same:
class Table(QtWidgets.QTableWidget):
def __init__(self):
super().__init__(20, 20)
self.scrollBarSpacer = QtWidgets.QWidget()
self.addScrollBarWidget(self.scrollBarSpacer, QtCore.Qt.AlignTop)
def resizeEvent(self, event):
super().resizeEvent(event)
self.scrollBarSpacer.setFixedHeight(self.horizontalHeader().height())
Do note that in both cases it's mandatory to call the base implementation of resizeEvent() first.
If you don't want to subclass, add an event filter for the table:
class SomeWindow(QtWidgets.QMainWindow):
def __init__(self):
# ...
self.table.installEventFilter(self)
def eventFilter(self, source, event):
if source == self.table and event.type() == QtCore.QEvent.Resize:
# let the table handle the event
source.event(event)
source.verticalScrollBar().setGeometry(
source.verticalScrollBar().geometry().adjusted(
0, self.horizontalHeader().height(), 0, 0))
return True
return super().eventFilter(source, event)
Related
I'm fairly new to PyQt
I'm trying to drawing a line from 1 QLabel to another.
My 2 QLabel are located on another QLabel which acts as an image in my GUI.
I've managed to track the mouse event and move the label around, but I cannot draw the line between them using QPainter.
Thank you in advance :)
This is my MouseTracking class
class MouseTracker(QtCore.QObject):
positionChanged = QtCore.pyqtSignal(QtCore.QPoint)
def __init__(self, widget):
super().__init__(widget)
self._widget = widget
self.widget.setMouseTracking(True)
self.widget.installEventFilter(self)
#property
def widget(self):
return self._widget
def eventFilter(self, o, e):
if e.type() == QtCore.QEvent.MouseMove:
self.positionChanged.emit(e.pos())
return super().eventFilter(o, e)
This is my DraggableLabel class:
class DraggableLabel(QLabel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.LabelIsMoving = False
self.setStyleSheet("border-color: rgb(238, 0, 0); border-width : 2.0px; border-style:inset; background: transparent;")
self.origin = None
# self.setDragEnabled(True)
def mousePressEvent(self, event):
if not self.origin:
# update the origin point, we'll need that later
self.origin = self.pos()
if event.button() == Qt.LeftButton:
self.LabelIsMoving = True
self.mousePos = event.pos()
# print(event.pos())
def mouseMoveEvent(self, event):
if event.buttons() == Qt.LeftButton:
# move the box
self.move(self.pos() + event.pos() - self.mousePos)
# print(event.pos())
def mouseReleaseEvent(self, event):
if event.button() == Qt.LeftButton:
print(event.pos())
def paintEvent(self, event):
painter = QPainter()
painter.setBrush(Qt.red)
# painter.setPen(qRgb(200,0,0))
painter.drawLine(10, 10, 200, 200)
This is my custom class for the QTabwigdet (since I need to control and track the position of 2 QLabels whenever the user add/insert a new Tab)
class DynamicTab(QWidget):
def __init__(self):
super(DynamicTab, self).__init__()
# self.count = 0
self.setMouseTracking(True)
self.setAcceptDrops(True)
self.bool = True
self.layout = QVBoxLayout(self)
self.label = QLabel()
self.layout.addChildWidget(self.label)
self.icon1 = DraggableLabel(parent=self)
#pixmap for icon 1
pixmap = QPixmap('icon1.png')
# currentTab.setLayout(QVBoxLayout())
# currentTab.layout.setWidget(QRadioButton())
self.icon1.setPixmap(pixmap)
self.icon1.setScaledContents(True)
self.icon1.setFixedSize(20, 20)
self.icon2 = DraggableLabel(parent=self)
pixmap = QPixmap('icon1.png')
# currentTab.setLayout(QVBoxLayout())
# currentTab.layout.setWidget(QRadioButton())
self.icon2.setPixmap(pixmap)
self.icon2.setScaledContents(True)
self.icon2.setFixedSize(20, 20)
#self.label.move(event.x() - self.label_pos.x(), event.y() - self.label_pos.y())
MainWindow and main method:
class UI_MainWindow(QMainWindow):
def __init__(self):
super(UI_MainWindow, self).__init__()
self.setWindowTitle("QHBoxLayout")
self.PictureTab = QTabWidget
def __setupUI__(self):
# super(UI_MainWindow, self).__init__()
self.setWindowTitle("QHBoxLayout")
loadUi("IIML_test2.ui", self)
self.tabChanged(self.PictureTab)
# self.tabChanged(self.tabWidget)
self.changeTabText(self.PictureTab, index=0, TabText="Patient1")
self.Button_ImportNew.clicked.connect(lambda: self.insertTab(self.PictureTab))
# self.PictureTab.currentChanged.connect(lambda: self.tabChanged(QtabWidget=self.PictureTab))
# self.tabWidget.currentChanged.connect(lambda: self.tabChanged(QtabWidget=self.tabWidget))
def tabChanged(self, QtabWidget):
QtabWidget.currentChanged.connect(lambda : print("Tab was changed to ", QtabWidget.currentIndex()))
def changeTabText(self, QTabWidget, index, TabText):
QTabWidget.setTabText(index, TabText)
def insertTab(self, QtabWidget):
# QFileDialog.getOpenFileNames(self, 'Open File', '.')
QtabWidget.addTab(DynamicTab(), "New Tab")
# get number of active tab
count = QtabWidget.count()
# change the view to the last added tab
currentTab = QtabWidget.widget(count-1)
QtabWidget.setCurrentWidget(currentTab)
pixmap = QPixmap('cat.jpg')
#currentTab.setLayout(QVBoxLayout())
#currentTab.layout.setWidget(QRadioButton())
# currentTab.setImage("cat.jpg")
currentTab.label.setPixmap(pixmap)
currentTab.label.setScaledContents(True)
currentTab.label.setFixedSize(self.label.width(), self.label.height())
tracker = MouseTracker(currentTab.label)
tracker.positionChanged.connect(self.on_positionChanged)
self.label_position = QtWidgets.QLabel(currentTab.label, alignment=QtCore.Qt.AlignCenter)
self.label_position.setStyleSheet('background-color: white; border: 1px solid black')
currentTab.label.show()
# print(currentTab.label)
#QtCore.pyqtSlot(QtCore.QPoint)
def on_positionChanged(self, pos):
delta = QtCore.QPoint(30, -15)
self.label_position.show()
self.label_position.move(pos + delta)
self.label_position.setText("(%d, %d)" % (pos.x(), pos.y()))
self.label_position.adjustSize()
# def SetupUI(self, MainWindow):
#
# self.setLayout(self.MainLayout)
if __name__ == '__main__':
app = QApplication(sys.argv)
UI_MainWindow = UI_MainWindow()
UI_MainWindow.__setupUI__()
widget = QtWidgets.QStackedWidget()
widget.addWidget(UI_MainWindow)
widget.setFixedHeight(900)
widget.setFixedWidth(1173)
widget.show()
try:
sys.exit(app.exec_())
except:
print("Exiting")
My concept: I have a DynamicTab (QTabWidget) which acts as a picture opener (whenever the user press Import Now). The child of this Widget are 3 Qlabels: self.label is the picture it self and two other Qlabels are the icon1 and icon2 which I'm trying to interact/drag with (Draggable Label)
My Problem: I'm trying to track my mouse movement and custom the painter to paint accordingly. I'm trying that out by telling the painter class to paint whenever I grab the label and move it with my mouse (Hence, draggable). However, I can only track the mouse position inside the main QLabel (the main picture) whenever I'm not holding or clicking my left mouse.
Any help will be appreciated here.
Thank you guys.
Painting can only happen within the widget rectangle, so you cannot draw outside the boundaries of DraggableLabel.
The solution is to create a further custom widget that shares the same parent, and then draw the line that connects the center of the other two.
In the following example I install an event filter on the two draggable labels which will update the size of the custom widget based on them (so that its geometry will always include those two geometries) and call self.update() which schedules a repainting. Note that since the widget is created above the other two, it might capture mouse events that are intended for the others; to prevent that, the Qt.WA_TransparentForMouseEvents attribute must be set.
class Line(QWidget):
def __init__(self, obj1, obj2, parent):
super().__init__(parent)
self.obj1 = obj1
self.obj2 = obj2
self.obj1.installEventFilter(self)
self.obj2.installEventFilter(self)
self.setAttribute(Qt.WA_TransparentForMouseEvents)
def eventFilter(self, obj, event):
if event.type() in (event.Move, event.Resize):
rect = self.obj1.geometry() | self.obj2.geometry()
corner = rect.bottomRight()
self.resize(corner.x(), corner.y())
self.update()
return super().eventFilter(obj, event)
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(painter.Antialiasing)
painter.setPen(QColor(200, 0, 0))
painter.drawLine(
self.obj1.geometry().center(),
self.obj2.geometry().center()
)
class DynamicTab(QWidget):
def __init__(self):
# ...
self.line = Line(self.icon1, self.icon2, self)
Notes:
to simplify things, I only use resize() (not setGeometry()), in this way the widget will always be placed on the top left corner of the parent and we can directly get the other widget's coordinates without any conversion;
the custom widget is placed above the other two because it is added after them; if you want to place it under them, use self.line.lower();
the painter must always be initialized with the paint device argument, either by using QPainter(obj) or painter.begin(obj), otherwise no painting will happen (and you'll get lots of errors in the output);
do not use layout.addChildWidget() (which is used internally by the layout), but the proper addWidget() function of the layout;
the stylesheet border syntax can be shortened with border: 2px inset rgb(238, 0, 0);;
the first lines of insertTab could be simpler: currentTab = DynamicTab() QtabWidget.addTab(currentTab, "New Tab");
currentTab.label.setFixedSize(self.label.size());
QMainWindow is generally intended as a top level widget, it's normally discouraged to add it to a QStackedWidget; note that if you did that because of a Youtube tutorial, that tutorial is known for suggesting terrible practices (like the final try/except block) which should not be followed;
only classes and constants should have capitalized names, not variables and functions which should always start with a lowercase letter;
I am trying to create a context menu (QMenu) for a right-click inside of PatternWidget(QWidget) with PySide6.
For this purpose I have overwritten contextMenuEvent as seen below.
For whatever reason, the context menu will NOT exit, when any of the actions in it or any space around it within PatternWidget is clicked, whether by left nor by right click.
The only way I can make the QMenu close is to click inside another widget or outside of the whole Application window.
I spent a long time on Google already and it seems that people usually have the opposite problem (keeping it open when clicking somewhere). I therefore think that something must be wrong in my code that prevents the default behavior to close the QMenu rather than me having to connect to some Signal and close it manually
I was also wondering if the Mouse Click event is not bubbling up until the QMenu but,the signals of the actions in the QMenu (Copy Row Up and Down) are firing as expected when clicked.
EDIT:
Removed QScrollArea
Added Code for PatternOverlay
class MainWindow(QMainWindow):
...
def __init_layout(self):
self.layout = QHBoxLayout()
self.layout.setContentsMargins(10, 10, 10, 10)
self.layout.setSpacing(1)
self.mainWidget = QWidget()
self.mainWidget.setLayout(self.layout)
self._scene = QtWidgets.QGraphicsScene(self)
self._view = QtWidgets.QGraphicsView(self._scene)
self._view.setStyleSheet("border: 0px; background-color: Gainsboro")
self.pattern_widget = PatternWidget()
self._scene.addWidget(self.pattern_widget)
self.layout.addWidget(self._view)
self.setCentralWidget(self.mainWidget)
class PatternOverlay(QWidget):
def __init__(self, parent):
super().__init__(parent=parent)
self.setAttribute(Qt.WA_NoSystemBackground)
self.setAttribute(Qt.WA_TransparentForMouseEvents)
self.rects = []
def paintEvent(self, _):
painter = QPainter(self)
painter.setPen(QPen(Qt.blue, 2, Qt.SolidLine))
for rect in self.rects:
painter.drawRect(rect)
painter.end()
self.rects.clear()
def addRect(self, rect):
self.rects.append(rect)
class PatternWidget(QWidget):
def __init__(self, cell_width, cell_height, stitches, rows):
super().__init__()
self.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
self.setMouseTracking(True)
# initialize overlay
self.__overlay = PatternOverlay(self)
self.__overlay.resize(self.size())
self.__overlay.show()
...
def contextMenuEvent(self, e):
copy_row_up_action = QAction("Copy Row Up", self)
copy_row_up_action.setStatusTip("Copy Row Up")
copy_row_up_action.triggered.connect(self.__handle_copy_row_up)
copy_row_down_action = QAction("Copy Row Down", self)
copy_row_down_action.setStatusTip("Copy Row Down")
copy_row_down_action.triggered.connect(self.__handle_copy_row_down)
context = QMenu(self)
context.addActions([copy_row_up_action, copy_row_down_action])
context.exec_(e.globalPos())
I'm having an issue where I have implemented a dragging feature for widgets using eventFilter(), but it seems when I drag towards the right and my cursor hovers over the widget which in my case is a QPushButton it seems to stop tracking until I hover out of the widget
How would I fix this?
class widget1(QWidget):
def __init__(self):
super().__init__()
self.widget = QPushButton("button0", self)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.createbutton = QPushButton('+', self)
self.createbutton.setGeometry(5, 5, 15, 15)
self.createbutton.clicked.connect(self.createWidget)
self.show()
def createWidget(self):
self.new_widget = widget1()
self.new_widget.setParent(self)
self.new_widget.show()
self.new_widget.installEventFilter(self)
def eventFilter(self, source, event):
if event.type() == QEvent.MouseMove:
MousePos = QPoint(event.pos())
if event.buttons() == Qt.LeftButton:
source.move(source.x() + MousePos.x(), source.y() + MousePos.y())
return super(MainWindow, self).eventFilter(source, event)
Basically I have a class with a widget in it called widget1() (This will be my custom widget in the future) I am then adding it dynamically to the window every time I press self.createbutton using .show() instead of using a layout, then it installs the event filter to it so that it can allow for dragging.
The event filter is receiving events for the widget1 instance but not for its child widgets (i.e. the QPushButton). A quick fix is to invoke QMouseEvent.ignore for the QPushButton's mouseMoveEvent so the mouse event will be propagated up to the parent widget and received in the event filter.
class widget1(QWidget):
def __init__(self):
super().__init__()
self.widget = QPushButton("button0", self)
self.widget.mouseMoveEvent = lambda event: event.ignore()
I am using PyQt and I'm trying to re-implement a QGraphicsTextItem, but it seems I'm missing something.
I would like to make the NodeTag item's text editable. I have tried setting flags such as Qt.TextEditorInteraction and QGraphicsItem.ItemIsMovable , but those seem to be ignored...
Here is a Minimal Reproducible Example :
import sys
from PyQt5.QtWidgets import QGraphicsScene, QGraphicsView, QMainWindow, QApplication, QGraphicsItem, QGraphicsTextItem
from PyQt5.QtCore import *
from PyQt5.QtGui import QPen
class NodeTag(QGraphicsTextItem):
def __init__(self,text):
QGraphicsTextItem.__init__(self,text)
self.text = text
self.setPos(0,0)
self.setTextInteractionFlags(Qt.TextEditorInteraction)
# self.setFlag(QGraphicsItem.ItemIsFocusable, True) # All these flags are ignored...
# self.setFlag(QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QGraphicsItem.ItemIsMovable, True)
def boundingRect(self):
return QRectF(0,0,80,25)
def paint(self,painter,option,widget):
painter.setPen(QPen(Qt.blue, 2, Qt.SolidLine))
painter.drawRect(self.boundingRect())
painter.drawText(self.boundingRect(),self.text)
def mousePressEvent(self, event):
print("CLICK!")
# self.setTextInteractionFlags(Qt.TextEditorInteraction) # make text editable on click
# self.setFocus()
class GView(QGraphicsView):
def __init__(self, parent, *args, **kwargs):
super().__init__(*args, **kwargs)
self.parent = parent
self.setGeometry(100, 100, 700, 450)
self.show()
class Scene(QGraphicsScene):
def __init__(self, parent):
super().__init__(parent)
self.parent = parent
tagItem = NodeTag("myText") # create a NodeTag item
self.addItem(tagItem)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__() # create default constructor for QWidget
self.setGeometry(900, 70, 1000, 800)
self.createGraphicView()
self.show()
def createGraphicView(self):
self.scene = Scene(self)
gView = GView(self)
scene = Scene(gView)
gView.setScene(scene)
# Set the main window's central widget
self.setCentralWidget(gView)
# Run program
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec_())
As you can see I have tried overriding the mousePressEvent and setting flags there too, but no luck so far.
Any help appreciated!
All QGraphicsItem subclasses have a paint method, and all items that paint some contents have that method overridden so that they can actually paint themselves.
The mechanism is the same as standard QWidgets, for which there is a paintEvent (the difference is that paint of QGraphicsItem receives an already instanciated QPainter), so if you want to do further painting other than what the class already provides, the base implementation must be called.
Consider that painting always happen from bottom to top, so everything that needs to be drawn behind the base painting has to be done before calling super().paint(), and everything that is going to be drawn in front of the default painting has to be placed after.
Depending on the situation, overriding might require that the default base implementation is called anyway, and that's important in your case for boundingRect too. QGraphicsTextItem automatically resizes itself when its contents change, so you should not always return a fixed QRect. If you need to have a minimum size, the solution is to merge a minimum rectangle with those provided by the default boundingRect() function.
Then, editing on a QGraphicsTextItem happens when the item gets focused, but since you also want to be able to move the item, things get trickier as both actions are based on mouse clicks. If you want to be able to edit the text with a single click, the solution is to make the item editable only when the mouse button has been released and has not been moved by some amount of pixels (the startDragDistance() property), otherwise the item is moved with the mouse. This obviously makes the ItemIsMovable flag useless, as we're going to take care of the movement internally.
Finally, since a minimum size is provided, we also need to override the shape() method in order to ensure that collision and clicks are correctly mapped, and return a QPainterPath that includes the whole bounding rect (for normal QGraphicsItem that should be the default behavior, but that doesn't happen with QGraphicsRectItem).
Here's a full implementation of what described above:
class NodeTag(QGraphicsTextItem):
def __init__(self, text):
QGraphicsTextItem.__init__(self, text)
self.startPos = None
self.isMoving = False
# the following is useless, not only because we are leaving the text
# painting to the base implementation, but also because the text is
# already accessible using toPlainText() or toHtml()
#self.text = text
# this is unnecessary too as all new items always have a (0, 0) position
#self.setPos(0, 0)
def boundingRect(self):
return super().boundingRect() | QRectF(0, 0, 80, 25)
def paint(self, painter, option, widget):
# draw the border *before* (as in "behind") the text
painter.setPen(QPen(Qt.blue, 2, Qt.SolidLine))
painter.drawRect(self.boundingRect())
super().paint(painter, option, widget)
def shape(self):
shape = QPainterPath()
shape.addRect(self.boundingRect())
return shape
def focusOutEvent(self, event):
# this is required in order to allow movement using the mouse
self.setTextInteractionFlags(Qt.NoTextInteraction)
def mousePressEvent(self, event):
if (event.button() == Qt.LeftButton and
self.textInteractionFlags() != Qt.TextEditorInteraction):
self.startPos = event.pos()
else:
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if self.startPos:
delta = event.pos() - self.startPos
if (self.isMoving or
delta.manhattanLength() >= QApplication.startDragDistance()):
self.setPos(self.pos() + delta)
self.isMoving = True
return
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
if (not self.isMoving and
self.textInteractionFlags() != Qt.TextEditorInteraction):
self.setTextInteractionFlags(Qt.TextEditorInteraction)
self.setFocus()
# the following lines are used to correctly place the text
# cursor at the mouse cursor position
cursorPos = self.document().documentLayout().hitTest(
event.pos(), Qt.FuzzyHit)
textCursor = self.textCursor()
textCursor.setPosition(cursorPos)
self.setTextCursor(textCursor)
super().mouseReleaseEvent(event)
self.startPos = None
self.isMoving = False
As a side note, remember that QGraphicsTextItem supports rich text formatting, so even if you want more control on the text painting process you should not use QPainter.drawText(), because you'd only draw the plain text. In fact, QGraphicsTextItem draws its contents using the drawContents() function of the underlying text document.
Try it:
...
class NodeTag(QGraphicsTextItem):
def __init__(self, text, parent=None):
super(NodeTag, self).__init__(parent)
self.text = text
self.setPlainText(text)
self.setFlag(QGraphicsItem.ItemIsMovable)
self.setFlag(QGraphicsItem.ItemIsSelectable)
def focusOutEvent(self, event):
self.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)
super(NodeTag, self).focusOutEvent(event)
def mouseDoubleClickEvent(self, event):
if self.textInteractionFlags() == QtCore.Qt.NoTextInteraction:
self.setTextInteractionFlags(QtCore.Qt.TextEditorInteraction)
super(NodeTag, self).mouseDoubleClickEvent(event)
def paint(self,painter,option,widget):
painter.setPen(QPen(Qt.blue, 2, Qt.SolidLine))
painter.drawRect(self.boundingRect())
# painter.drawText(self.boundingRect(),self.text)
super().paint(painter, option, widget)
...
Here is some code that illustrates my problem:
import sys
from PyQt4 import QtGui, QtCore
class CustomButton(QtGui.QAbstractButton):
def __init__(self, *__args):
super().__init__(*__args)
self.setFixedSize(190, 50)
self.installEventFilter(self)
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.setBrush(QtGui.QColor(136, 212, 78))
painter.setPen(QtCore.Qt.NoPen)
painter.drawRect(QtCore.QRect(0, 0, 100, 48))
def eventFilter(self, object, event):
if event.type() == QtCore.QEvent.HoverMove:
painter = QtGui.QPainter(self)
painter.begin(self)
painter.drawRect(QtCore.QRect(0, 0, 100, 48))
painter.end()
return True
return False
app = QtGui.QApplication(sys.argv)
window = QtGui.QWidget()
layout = QtGui.QGridLayout(window)
button = CustomButton()
layout.addWidget(button, 0, 0)
window.show()
sys.exit(app.exec_())
The goal is to make a button using QPainter that can be modified when the HoverMove event is detected. However, I get the following errors upon hovering:
QPainter::begin: Paint device returned engine == 0, type: 1
QPainter::begin: Paint device returned engine == 0, type: 1
QPainter::drawRects: Painter not active
QPainter::end: Painter not active, aborted
From what I've understood from the docs (here) I can use .begin() to activate the QPainter; however as the error message shows this isn't the case and the second rectangle does not get drawn. How should I use QPainter to achieve the desired output ?
You need to detect the hover inside paintEvent and act accordingly:
def paintEvent(self, event):
option = QtGui.QStyleOptionButton()
option.initFrom(self)
painter = QtGui.QPainter(self)
if option.state & QtGui.QStyle.State_MouseOver:
# do hover stuff ...
else:
# do normal stuff ...
QStyleOption and its subclasses contain all the information that QStyle functions need to draw a graphical element. The QPaintEvent only contains information about what area needs to be updated.
#ekhumoro's answer above can be rewritten more simply with QWidget::underMouse(). the docs
def paintEvent(self, event):
if underMouse():
# do hover stuff ...
else:
# do normal stuff ...