I'm adding a color parameter to the LineBand subclass of QWidget. I've found several examples of how to add additional parameters to a subclass in Python 3 and believe I've followed the advice. Yet, when I call the new version of the class using box = LineBand(self.widget2, color), I get the error File "C:/Users/...", line 63, in showBoxes ... box = LineBand(viewport, color) ... TypeError: __init__() takes 2 positional arguments but 3 were given. But, I'm only calling LineBand with 2 arguments, right? Below is the complete code. I've commented all the sections I've changed. I've also commented out the code that changes the background color of the text in order to see the colored lines more clearly (when they actually are drawn). The background color code works fine.
import sys
from PySide.QtCore import *
from PySide.QtGui import *
db = ((5,8,'A',Qt.darkMagenta),(20,35,'B',Qt.darkYellow),(45,60,'C',Qt.darkCyan)) # added color to db
class TextEditor(QTextEdit):
def __init__(self, parent=None):
super().__init__(parent)
text="This is example text that is several lines\nlong and also\nstrangely broken up and can be\nwrapped."
self.setText(text)
cursor = self.textCursor()
for n in range(0,len(db)):
row = db[n]
startChar = row[0]
endChar = row[1]
id = row[2]
color = row[3] # assign color from db to variable
cursor.setPosition(startChar)
cursor.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor, endChar-startChar)
#charfmt = cursor.charFormat()
#charfmt.setBackground(QColor(color)) # assign color to highlight (background)
#cursor.setCharFormat(charfmt)
cursor.clearSelection()
self.setTextCursor(cursor)
def getBoundingRect(self, start, end):
cursor = self.textCursor()
cursor.setPosition(end)
last_rect = end_rect = self.cursorRect(cursor)
cursor.setPosition(start)
first_rect = start_rect = self.cursorRect(cursor)
if start_rect.y() != end_rect.y():
cursor.movePosition(QTextCursor.StartOfLine)
first_rect = last_rect = self.cursorRect(cursor)
while True:
cursor.movePosition(QTextCursor.EndOfLine)
rect = self.cursorRect(cursor)
if rect.y() < end_rect.y() and rect.x() > last_rect.x():
last_rect = rect
moved = cursor.movePosition(QTextCursor.NextCharacter)
if not moved or rect.y() > end_rect.y():
break
last_rect = last_rect.united(end_rect)
return first_rect.united(last_rect)
class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.edit = TextEditor(self)
layout = QVBoxLayout(self)
layout.addWidget(self.edit)
self.boxes = []
def showBoxes(self):
while self.boxes:
self.boxes.pop().deleteLater()
viewport = self.edit.viewport()
for start, end, id, color in db: # get color too
rect = self.edit.getBoundingRect(start, end)
box = LineBand(viewport, color) # call LineBand with color as argument
box.setGeometry(rect)
box.show()
self.boxes.append(box)
def resizeEvent(self, event):
self.showBoxes()
super().resizeEvent(event)
class LineBand(QWidget):
def __init__(self, color): # define color within __init__
super().__init__(self)
self.color = color
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
painter.setPen(QPen(color, 1.8)) # call setPen with color
painter.drawLine(self.rect().topLeft(), self.rect().bottomRight())
if __name__ == '__main__':
app = QApplication(sys.argv)
window = Window()
window.show()
window.showBoxes()
app.exec_()
sys.exit(app.exec_())
When a method is not overwritten it will be the same as the implemented method of the parent so if you want it to work you must add those parameters, since these depend many times on the parent a simple way is to use *args and **kwargs and pass the new parameter as the first parameter. In addition you must use self.color instead of color since color only exists in the constructor.
class Window(QWidget):
[...]
def showBoxes(self):
while self.boxes:
self.boxes.pop().deleteLater()
viewport = self.edit.viewport()
for start, end, id, color in db: # get color too
rect = self.edit.getBoundingRect(start, end)
box = LineBand(color, viewport) # call LineBand with color as argument
box.setGeometry(rect)
box.show()
self.boxes.append(box)
[...]
class LineBand(QWidget):
def __init__(self, color, *args, **kwargs):
QWidget.__init__(self, *args, **kwargs)
self.color = color
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
painter.setPen(QPen(self.color, 1.8)) # call setPen with color
painter.drawLine(self.rect().topLeft(), self.rect().bottomRight())
Output:
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;
How to animate geometry change inside QGridLayout. I have QLabel which will be placed inside the QGridlayout. It should expand when the mouse is inside the QLabel and shrink back to the normal state when outside. I managed to animate but, it doesn't expand from all four sides. It instead moves away from the grid.
MRE:
import sys
from PyQt5 import QtWidgets, QtCore
class Tile(QtWidgets.QLabel):
def __init__(self, *args, **kwargs):
super(Tile, self).__init__(*args, **kwargs)
# p = self.palette()
# p.setColor(self.backgroundRole(), QtCore.Qt.red)
# self.setPalette(p)
self.setText("hello")
self.setMinimumSize(100, 100)
self.setMaximumSize(125, 125)
def enterEvent(self, a0: QtCore.QEvent) -> None:
super(Tile, self).enterEvent(a0)
self.animation = QtCore.QPropertyAnimation(self, b"geometry")
self.animation.setStartValue(QtCore.QRect(self.geometry()))
self.animation.setEndValue(QtCore.QRect(self.geometry().adjusted(-25, -25, 25, 25)))
self.animation.setDuration(150)
self.animation.start(QtCore.QPropertyAnimation.DeleteWhenStopped)
def leaveEvent(self, a0: QtCore.QEvent) -> None:
super(Tile, self).leaveEvent(a0)
self.animation = QtCore.QPropertyAnimation(self, b"geometry")
self.animation.setStartValue(QtCore.QRect(self.geometry()))
self.animation.setEndValue(QtCore.QRect(self.geometry().adjusted(25, 25, -25, -25)))
self.animation.setDuration(150)
self.animation.start(QtCore.QPropertyAnimation.DeleteWhenStopped)
class ScrollView(QtWidgets.QWidget):
def __init__(self, *args, **kwargs):
super(ScrollView, self).__init__(*args, **kwargs)
self.setStyleSheet('border: 1px solid black')
self.setLayout(QtWidgets.QVBoxLayout())
widget = QtWidgets.QWidget()
self.grid_layout = QtWidgets.QGridLayout(widget)
self.scrollArea = QtWidgets.QScrollArea()
self.scrollArea.setWidget(widget)
self.scrollArea.setWidgetResizable(True)
self.grid_layout.setSpacing(50)
self.row_width = 4
self._row = 0
self._column = 0
self.layout().addWidget(self.scrollArea)
def addTile(self):
self.grid_layout.addWidget(Tile(), self._row, self._column)
if self._column == 3:
self._row += 1
self._column = 0
else:
self._column += 1
def main():
app = QtWidgets.QApplication(sys.argv)
win = ScrollView()
for x in range(30):
win.addTile()
win.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
The main problem is that you're setting a maximum size that is smaller than the end value: if you have a starting rectangle with size 100x100 and you expand it 25 pixels on each side, it will become 150x150, not 125x125. Since you've set that maximum value, once the geometry reaches 125x125 it will only change the coordinates while keeping that maximum size.
But there are also three other issues.
you're always using the current geometry as start value, which can become an issue: if you enter or leave while the other animation is in progress, you get a wrong reference for the target geometry;
if you enter/leave the widget very fast, you'll end up with two concurrent animations taking place; there's literally no benefit in continously creating animations every time;
the widget doesn't take into account resizing of the scroll area, which would alter the aspect ratio and create issues for the new positioning;
In order to avoid all this, you have to use one animation only, change its direction according to the enter/leave event, properly modify the start/end values according to the actual geometry changes, and also properly resize to the "default" size when an external resize happens; the last two points are done only if the animation is not active (since moveEvent and resizeEvent are called by the geometry change).
class Tile(QtWidgets.QLabel):
def __init__(self, *args, **kwargs):
super(Tile, self).__init__(*args, **kwargs)
self.setText("hello")
self.setMinimumSize(100, 100)
self.setMaximumSize(150, 150)
self.animation = QtCore.QPropertyAnimation(self, b"geometry")
self.animation.setDuration(150)
def animate(self, expand):
if expand:
self.animation.setDirection(self.animation.Forward)
else:
self.animation.setDirection(self.animation.Backward)
self.animation.start()
def enterEvent(self, a0: QtCore.QEvent) -> None:
super(Tile, self).enterEvent(a0)
self.animate(True)
def leaveEvent(self, a0: QtCore.QEvent) -> None:
super(Tile, self).leaveEvent(a0)
self.animate(False)
def updateAnimation(self):
if not self.animation.state():
center = self.geometry().center()
start = QtCore.QRect(QtCore.QPoint(), self.minimumSize())
start.moveCenter(center)
self.animation.setStartValue(start)
end = QtCore.QRect(QtCore.QPoint(), self.maximumSize())
end.moveCenter(center)
self.animation.setEndValue(end)
def moveEvent(self, event):
self.updateAnimation()
def resizeEvent(self, event):
self.updateAnimation()
if not self.animation.state():
rect = QtCore.QRect(QtCore.QPoint(),
self.maximumSize() if self.underMouse() else self.minimumSize())
rect.moveCenter(self.geometry().center())
self.setGeometry(rect)
I am attempting to design a label class that inherits from the PyQt5 base QLabel class that is able to track another widget. Here is the current code for my class:
class AttachedLabel(QLabel):
def __init__(self, attachedTo, *args, side="left", ** kwargs):
super().__init__(*args, **kwargs) # Run parent initialization
# Define instance variables
self.attached = attachedTo
self.side = side
# Update label position
self.updatePos()
def updatePos(self):
# Get "attached widget" position and dimensions
x = self.attached.geometry().x()
y = self.attached.geometry().y()
aWidth = self.attached.geometry().width()
aHeight = self.attached.geometry().height()
# Get own dimensions
width = self.geometry().width()
height = self.geometry().height()
if self.side == "top": # Above of attached widget
self.setGeometry(x, y-height, width, height)
elif self.side == "bottom": # Below attached widget
self.setGeometry(x, y+height+aHeight, width, height)
elif self.side == "right": # Right of attached widget
self.setGeometry(x + width + aWidth, y, width, height)
else: # Left of attached widget
self.setGeometry(x - width, y, width, height)
I want to be able to instantiate the label like so:
AttachedLabel(self.pushButton, self.centralwidget)
where self.pushButton is the widget it is supposed to be following. The issue is that I don't know how to detect when the widget moves in order to run my updatePos() function. I would ideally only update the label position when the other widget moves, but I want to refrain from havign to add extra code to the class of the widget that is being tracked. I have tried overriding the paintEvent, but that only triggers when the object itself needs to be redrawn, so it doesn't even function as a sub-optimal solution.
Is there some built-in method I can use/override to detect when the widget moves or when the screen itself is updated?
You have to use an eventFilter intersecting the QEvent::Move event and you should also track the resize through the QEvent::Resize event.
from dataclasses import dataclass, field
import random
from PyQt5 import QtCore, QtWidgets
class GeometryTracker(QtCore.QObject):
geometryChanged = QtCore.pyqtSignal()
def __init__(self, widget):
super().__init__(widget)
self._widget = widget
self.widget.installEventFilter(self)
#property
def widget(self):
return self._widget
def eventFilter(self, source, event):
if self.widget is source and event.type() in (
QtCore.QEvent.Move,
QtCore.QEvent.Resize,
):
self.geometryChanged.emit()
return super().eventFilter(source, event)
#dataclass
class TrackerManager:
widget1: field(default_factory=QtWidgets.QWidget)
widget2: field(default_factory=QtWidgets.QWidget)
alignment: QtCore.Qt.Alignment = QtCore.Qt.AlignLeft
enabled: bool = True
valid_alignments = (
QtCore.Qt.AlignLeft,
QtCore.Qt.AlignRight,
QtCore.Qt.AlignHCenter,
QtCore.Qt.AlignTop,
QtCore.Qt.AlignBottom,
QtCore.Qt.AlignVCenter,
)
def __post_init__(self):
self._traker = GeometryTracker(self.widget1)
self._traker.geometryChanged.connect(self.update)
if not any(self.alignment & flag for flag in self.valid_alignments):
raise ValueError("alignment is not valid")
def update(self):
if not self.enabled:
return
r = self.widget1.rect()
p1 = r.center()
c1 = r.center()
if self.alignment & QtCore.Qt.AlignLeft:
p1.setX(r.left())
if self.alignment & QtCore.Qt.AlignRight:
p1.setX(r.right())
if self.alignment & QtCore.Qt.AlignTop:
p1.setY(r.top())
if self.alignment & QtCore.Qt.AlignBottom:
p1.setY(r.bottom())
p2 = self.convert_position(p1)
c2 = self.convert_position(c1)
g = self.widget2.geometry()
g.moveCenter(c2)
if self.alignment & QtCore.Qt.AlignLeft:
g.moveRight(p2.x())
if self.alignment & QtCore.Qt.AlignRight:
g.moveLeft(p2.x())
if self.alignment & QtCore.Qt.AlignTop:
g.moveBottom(p2.y())
if self.alignment & QtCore.Qt.AlignBottom:
g.moveTop(p2.y())
self.widget2.setGeometry(g)
def convert_position(self, point):
gp = self.widget1.mapToGlobal(point)
if self.widget2.isWindow():
return gp
return self.widget2.parent().mapFromGlobal(gp)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.button = QtWidgets.QPushButton("Press me", self)
self.label = QtWidgets.QLabel(
"Tracker\nLabel", self, alignment=QtCore.Qt.AlignCenter
)
self.label.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents, True)
self.label.setFixedSize(200, 200)
self.label.setStyleSheet(
"background-color: salmon; border: 1px solid black; font-size: 40pt;"
)
self.resize(640, 480)
self.manager = TrackerManager(
widget1=self.button,
widget2=self.label,
alignment=QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter,
)
self.move_button()
def move_button(self):
pos = QtCore.QPoint(*random.sample(range(400), 2))
animation = QtCore.QPropertyAnimation(
targetObject=self.button,
parent=self,
propertyName=b"pos",
duration=1000,
startValue=self.button.pos(),
endValue=pos,
)
animation.finished.connect(self.move_button)
animation.start(QtCore.QAbstractAnimation.DeleteWhenStopped)
def main():
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
I'm making an application that shows a map of an area and I'm trying to draw nodes on top of it which can represent information.
I made it all work, but did so simply by making one custom widget which I showed and printing everything again and again everytime information changed. Also I couldn't 'connect' the nodes to listeners, because they were just images in the original widget.
This made me want to reform my GUI and now I'm trying to make every class a custom widget! But there's a problem, my MapNodes aren't showing up anymore.
I searched stackoverflow and found this helpful thread:
How to set absolute position of the widgets in qt
So I have to give my mapnodes a parent, and parent = the widget that is being shown (?)
Anyway, here is my throw at it, pasting the relevant code here. Hint at where stuff might go horrible wrong: all the inits
app = QtGui.QApplication(list())
mutexbranch = Lock()
mutexnode = Lock()
def exec():
return app.exec_()
#Singleton Pattern: wanneer en object aan iets moet kunnen
# waar het inherent door de structuur niet aankon
# wordt dit via dit singleton opgelost
class GuiInternalCommunication:
realmap = 0
class MapView(QtGui.QWidget, listener.Listener):
def __init__(self, mapimagepath):
QtGui.QMainWindow.__init__(self)
listener.Listener.__init__(self)
self.map = Map(self, mapimagepath)
#self.setCentralWidget(self.map)
self.initUI()
def initUI(self):
self.setWindowTitle('Population mapping')
hbox = QtGui.QHBoxLayout()
hbox.addWidget(self.map)
self.setLayout(hbox)
resolution = QtGui.QDesktopWidget().screenGeometry()
self.setGeometry(20,20,550,800)
self.show()
######################################################################
class Map(QtGui.QWidget):
def __init__(self, parent, mapimagepath):
QtGui.QWidget.__init__(self, parent)
#self.timer = QtCore.QBasicTimer()
#coordinaten hoeken NE en SW voor kaart in map graphics van SKO
self.realmap = RealMap(
mapimagepath,
(51.0442, 3.7268),
(51.0405, 3.7242),
550,
800)
GuiInternalCommunication.realmap = self.realmap
self.needsupdate = True
self.timelabel = 0
parent.setGeometry(0,0,self.realmap.width, self.realmap.height)
self.mapNodes = {}
self.mapBranches = {}
def paintEvent(self, event):
painter = QtGui.QPainter()
painter.begin(self)
rect = self.contentsRect()
#teken achtergrond
self.realmap.drawRealMap(painter)
#teken branches
mutexbranch.acquire()
try:
for branch, mapBranch in self.mapBranches.items():
mapBranch.drawMapBranch(painter)
finally:
mutexbranch.release()
######################################################################
class RealMap(QtGui.QWidget):
def __init__(self, path, coordRightTop, coordLeftBot, width, height, pixpermet = 2.6):
super(RealMap, self).__init__()
self.path = path
self.mapimage = QtGui.QImage(self.path)
self.coordLeftBot = coordLeftBot
self.coordRightTop = coordRightTop
self.width = width
self.height = height
self.realdim = self.calcRealDim()
self.pixpermet = pixpermet
def paintEvent(self, e):
painter = QtGui.QPainter()
painter.begin(self)
self.drawRealMap(self, painter)
painter.end()
def drawRealMap(self, painter):
painter.drawImage(0,0,self.mapimage)
######################################################################
class MapNode(QtGui.QWidget):
dangertocolor = {"normal":"graphics//gradients//green.png",
"elevated":"graphics//gradients//orange.png",
"danger":"graphics//gradients//red.png"}
gradimage = {"normal":QtGui.QImage(dangertocolor["normal"]),
"elevated":QtGui.QImage(dangertocolor["elevated"]),
"danger":QtGui.QImage(dangertocolor["danger"])}
btimage = QtGui.QImage("graphics//BT-icon.png")
def __init__(self, scanner, x, y, danger = 0, parent = None):
# MapNode erft over van QWidget
super(MapNode, self).__init__()
QtGui.QWidget.__init__(self, parent)
self.scanner = scanner
self.x = x
self.y = y
self.danger = 'normal'
self.calcDanger(danger)
self.grads = {}
self.grad = QtGui.QImage(MapNode.dangertocolor[self.danger])
def paintEvent(self, e):
painter = QtGui.QPainter()
painter.begin(self)
self.drawMapNode(painter)
painter.end()
def drawMapNode(self, painter):
realmap = GuiInternalCommunication.realmap
radiusm = self.scanner.range
radiusp = radiusm*realmap.pixpermet
factor = radiusp/200 # basis grootte gradiƫnten is 200 pixels.
grad = MapNode.gradimage[self.danger]
grad = grad.scaled(grad.size().width()*factor, grad.size().height()*factor)
painter.drawImage(self.x-100*factor,self.y-100*factor, grad)
painter.drawImage(self.x-10, self.y-10,MapNode.btimage)
painter.drawText(self.x-15, self.y+20, str(self.scanner.sensorid) + '-' + str(self.scanner.name))
######################################################################
class MapBranch:
branchpens = {"normal": QtGui.QPen(QtCore.Qt.green, 3, QtCore.Qt.DashLine),
"elevated": QtGui.QPen(QtGui.QColor(255, 51, 0), 3, QtCore.Qt.DashLine), #mandarine orange hex is 255-165-0
"danger": QtGui.QPen(QtCore.Qt.red, 3, QtCore.Qt.DashLine)}
def __init__(self, branch, mapnode1, mapnode2, danger = 0):
self.mapnode1 = mapnode1
self.mapnode2 = mapnode2
self.branch = branch
self.danger = danger
self.calcDanger(danger)
def drawMapBranch(self, painter):
painter.setPen(MapBranch.branchpens[self.danger])
painter.drawLine(self.mapnode1.x,
self.mapnode1.y,
self.mapnode2.x,
self.mapnode2.y)
EDIT - I forgot to add the code that adds the nodes. So after an event comes in the node needs to be created, this method fires creating the node:
def addNode(self, scanner):
mutexnode.acquire()
try:
coord = self.realmap.convertLatLon2Pix((scanner.latitude, scanner.longitude))
self.mapNodes[scanner.sensorid] = MapNode(scanner, coord[0], coord[1], parent = self)
self.mapNodes[scanner.sensorid].move(coord[0],coord[1])
#self.mapNodes[scanner.sensorid].show()
finally:
mutexnode.release()
I would recommend you to use the QGraphicsScene and QGraphicsItem classes for your map instead of normal QWidget classes, since they are made exactly for the purpose of displaying a large number of graphical items:
QGraphicsScene http://doc.qt.io/qt-5/qgraphicsscene.html
QGraphicsItem: http://doc.qt.io/qt-5/qgraphicsitem-members.html
From the documentation:
The QGraphicsScene class provides a surface for managing a large number of 2D graphical items.
The class serves as a container for QGraphicsItems. It is used together with QGraphicsView for visualizing graphical items, such as lines, rectangles, text, or even custom items, on a 2D surface. QGraphicsScene is part of the Graphics View Framework.
QGraphicsScene also provides functionality that lets you efficiently determine both the location of items, and for determining what items are visible within an arbitrary area on the scene. With the QGraphicsView widget, you can either visualize the whole scene, or zoom in and view only parts of the scene.
You can also embed widgets derived from QWidget in the scene, which should allow you to display practically any kind of information. As a bonus, you will get layering, fast transformations and ready-to-use handling of mouse interactions, which should be quite useful for realizing an interactive map.
I am just trying my first prototype in pyside (python/Qt). The application itself starts up fine, creates a window with widgets according to my layout. Threads are started and execute, all fine. Except...
I want to enhance the GUI by adding some custom widget indicating the execution state of the threads. So I thought flashing LEDs would be fine for that. For this I try to implement a custom LED widget.
Remember that I currently try to learn python, so there might be some strange approaches in this. Anyway, here is the LED widgets class in its current state:
from PySide import QtCore, QtGui
class LED(QtGui.QWidget):
class Mode:
STATIC_OFF = 0
STATIC_ON = 1
FLASH_SLOW = 2
FLASH_MEDIUM = 2
FLASH_FAST = 3
class Color:
BLACK = '#000000'
GREEN = '#00FF00'
RED = '#FF0000'
BLUE = '#0000FF'
YELLOW = '#FFFF00'
WHITE = '#FFFFFF'
mode = Mode.STATIC_ON
color = Color.BLACK
radius = 10
status = False
timer = None
outdated = QtCore.Signal()
def __init__(self, mode, color, radius, parent=None):
super(LED, self).__init__(parent)
self.outdated.connect(self.update)
self.setMode(mode,False)
self.setColor(color,False)
self.setRadius(radius,False)
self.timer = QtCore.QTimer(self)
self.timer.timeout.connect(self.adjustAppearance)
self.adjustAppearance()
def getCenter(self):
return QtCore.QPoint(self.radius, self.radius)
def getBox(self):
return QtCore.QRect(self.radius, self.radius)
def setColor(self, color, update=True):
assert color in (self.Color.GREEN,self.Color.RED,self.Color.BLUE,self.Color.YELLOW,self.Color.WHITE), "invalid color"
self.color = color
if update:
self.adjustAppearance()
def setMode(self, mode, update=True):
assert mode in (self.Mode.STATIC_OFF,self.Mode.STATIC_ON,self.Mode.FLASH_SLOW,self.Mode.FLASH_MEDIUM,self.Mode.FLASH_FAST),"invalid mode"
self.mode = mode
if update:
self.adjustAppearance()
def setRadius(self, radius, update=True):
assert isinstance(radius, int), "invalid radius type (integer)"
assert 10<=radius<=100, "invalid radius value (0-100)"
self.radius = radius
if update:
self.adjustAppearance()
def switchOn(self):
self.status = True
self.adjustAppearance()
def switchOff(self):
self.status = False
self.adjustAppearance()
def adjustAppearance(self):
if self.mode is self.Mode.STATIC_OFF:
self.status = False
self.timer.stop()
elif self.mode is self.Mode.STATIC_ON:
self.status = True
self.timer.stop()
elif self.mode is self.Mode.FLASH_SLOW:
self.status = not self.status
self.timer.start(200)
elif self.mode is self.Mode.FLASH_SLOW:
self.status = not self.status
self.timer.start(500)
elif self.mode is self.Mode.FLASH_SLOW:
self.status = not self.status
self.timer.start(1000)
self.outdated.emit()
def paintEvent(self, event):
painter = QtGui.QPainter()
painter.begin(self)
self.drawWidget(event, painter)
painter.end()
def drawWidget(self, event, painter):
if self.status:
shade = QtGui.QColor(self.color).darker
else:
shade = QtGui.QColor(self.color).lighter
#painter.setPen(QtGui.QColor('black'), 1, QtCore.Qt.SolidLine)
painter.setPen(QtGui.QColor('black'))
painter.setBrush(QtCore.Qt.RadialGradientPattern)
painter.drawEllipse(self.getCenter(), self.radius, self.radius)
My problem is that the widget simply does not show when I add it to the windows layout. Other widgets (non-custome, plain Qt widgets) do show, so I gues it is not a question of creating the widget, not a question of how I use the widget. Nevertheless here is the (shortened) instanciation if the widget:
class View(QtGui.QMainWindow):
ui = None
def __init__(self, config, parent=None):
log.debug("window setup")
self.config = config
super(View, self).__init__(parent)
try:
self.ui = self.Ui(self)
self.setObjectName("appView")
self.setWindowTitle("AvaTalk")
self.show()
except RuntimeError as e:
log.error(e.message)
class Ui(QtCore.QObject):
# [...]
iconDetector = None
buttonDetector = None
# [...]
def __init__(self, window, parent=None):
log.debug("ui setup")
super(View.Ui, self).__init__(parent)
self.window = window
# central widget
log.debug("widget setup")
self.centralWidget = QtGui.QWidget()
self.widgetLayout = QtGui.QVBoxLayout(self.centralWidget)
# create toolbars
#self.createMenubar()
#self.createCanvas()
self.createToolbar()
#self.createStatusbar()
# visualize widget
self.window.setCentralWidget(self.centralWidget)
# actions
log.debug("actions setup")
self.actionQuit = QtGui.QAction(self.window)
self.actionQuit.setObjectName("actionQuit")
self.menuFile.addAction(self.actionQuit)
self.menubar.addAction(self.menuFile.menuAction())
log.debug("connections setup")
QtCore.QObject.connect(self.actionQuit, QtCore.SIGNAL("activated()"), self.window.close)
QtCore.QMetaObject.connectSlotsByName(self.window)
def createToolbar(self):
log.debug("toolbar setup")
self.toolbar = QtGui.QHBoxLayout()
self.toolbar.setObjectName("toolbar")
self.toolbar.addStretch(1)
# camera
# detector
self.iconDetector = LED(LED.Mode.STATIC_OFF,LED.Color.GREEN,10,self.window)
self.buttonDetector = IconButton("Detector", "detector",self.window)
self.toolbar.addWidget(self.iconDetector)
self.toolbar.addWidget(self.buttonDetector)
self.toolbar.addStretch(1)
# analyzer
# extractor
# layout
self.widgetLayout.addLayout(self.toolbar)
It might well be that the actual painting using QPainter is still nonsense. I did not yet come to test that: actually when testing I find that isVisible() returns False on the widget after the setup has completed. So I assume I miss a central point. Unfortunately I am unable to find out what I miss...
Maybe someone can spot my issue? Thanks !
One thing to be careful when implementing custom widgets derived from QWidget is: sizeHint or minimumSizeHint for QWidget returns invalid QSize by default. This means, if it is added to a layout, depending on the other widgets, it will shrink to 0. This effectively makes it 'not-visible'. Although, isVisible would still return True. Widget is 'visible', but it just doesn't have anything to show (0 size). So, if you're getting False, there is definitely another issue at hand.
So it is necessary to define these two methods with sensible sizes:
class LED(QtGui.QWidget):
# ...
def sizeHint(self):
size = 2 * self.radius + 2
return QtCore.QSize(size, size)
def minimumSizeHint(self):
size = 2 * self.radius + 2
return QtCore.QSize(size, size)
Note: There are other issues:
Like defining mode, color, etc as class attributes and then overriding them with instance attributes. They won't break anything but they are pointless.
painter.setBrush(QtCore.Qt.RadialGradientPattern) is wrong. You can't create a brush with QtCore.Qt.RadialGradientPattern. It is there, so that brush.style() can return something. If you want a gradient pattern you should create a brush with QGradient constructor.
if self.mode is self.Mode.STATIC_OFF: comparing with is is wrong. is compares identity, you want == here. (also, FLASH_SLOW and FLASH_MEDIUM are both 2)
assert is for debugging and unit tests. You shouldn't use it in real code. Raise an exception if you want. Or have sensible defaults, where invalid values would be replaced with that.