Python Qt bindings: setCosmetic() and sceneRect(), problems with margins - python

With the following simple example (which works well with either PySide or PyQt4):
import sys
import random
import numpy
from PySide import QtGui, QtCore
class Window(QtGui.QWidget):
def __init__(self):
super(Window, self).__init__()
self.resize(600, 400)
self.view = QtGui.QGraphicsView()
self.scene = QtGui.QGraphicsScene()
self.view.setScene(self.scene)
self.setWindowTitle('Example')
# Layout
layout = QtGui.QGridLayout()
layout.addWidget(self.view, 0, 0)
self.setLayout(layout)
# Styles
self.pen = QtGui.QPen(QtCore.Qt.black, 0, QtCore.Qt.SolidLine)
self.brush = QtGui.QBrush(QtGui.QColor(255, 255, 255, 0))
def addLine(self, x0, y0, x1, y1):
line = QtCore.QLineF(x0, -y0, x1, -y1)
pen = QtGui.QPen(QtGui.QColor(0, 0, 255, 255), 0, QtCore.Qt.SolidLine)
l = self.scene.addLine(line, pen)
def addRect(self, left, top, width, height):
rect = QtCore.QRectF(left, -top, width, abs(height))
r = self.scene.addRect(rect, self.pen, self.brush)
def fit(self):
self.view.fitInView(self.scene.sceneRect())
def resizeEvent(self, event = None):
self.fit()
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
window = Window()
window.show()
window.addLine(-1, -1, 2, 2)
window.addLine(0, 1, 1, 0)
window.addRect(0, 1, 1, 1)
window.fit()
sys.exit(app.exec_())
I am able to draw one rectangle and two blue lines crossing it. Notice how, using a QtGui.QPen with width 0 makes the blue lines have a constant width no matters the size of the square/lines nor the size of the window:
That is because a zero width pen is cosmetic by default. According to the Qt documentation:
Cosmetics pens are used to draw strokes that have a constant width
regarless of any transformations applied to the QPainter they are
used with. Drawing a shape with a cosmetic pen ensures that its outline
will have the same thickness at different scale factors.
A non-zero width pen can be also set as cosmetic, using the method setCosmetic(True). So, setting the pen width in the example's addLine() method to 2 and setting the pen as cosmetic:
def addLine(self, x0, y0, x1, y1):
line = QtCore.QLineF(x0, -y0, x1, -y1)
pen = QtGui.QPen(QtGui.QColor(0, 0, 255, 255), 2, QtCore.Qt.SolidLine)
pen.setCosmetic(True)
l = self.scene.addLine(line, pen)
Gives the following output:
As you can see, the margins are huge, and I really would expect the crossing line to start and end at the lower-left and upper-right corners of the window, with no margins.
It seems that those margins were added because the scene.sceneRect() was modified as if the line was not cosmetic. See for example this case, in which the width is set to 2 as well, but the pen is not set as cosmetic:
def addLine(self, x0, y0, x1, y1):
line = QtCore.QLineF(x0, -y0, x1, -y1)
pen = QtGui.QPen(QtGui.QColor(0, 0, 255, 255), 2, QtCore.Qt.SolidLine)
l = self.scene.addLine(line, pen)
Why is this happening? Shouldn't the extra margins be added only when isCosmetic() == False? If this behavior is intentional, could someone explain the reason?
Also, is there a way to avoid it? Something "clean", different from manually changing the boundings of the line before adding it to the scene (or different from substracting the extra margins later from the scene). Perhaps there is a configuration parameter or another way of adding the line to the scene?
EDIT
Setting the cap style to "flat" results in smaller margins, although the problem is still there:
def addLine(self, x0, y0, x1, y1):
line = QtCore.QLineF(x0, -y0, x1, -y1)
pen = QtGui.QPen(QtGui.QColor(0, 0, 255, 255), 2, QtCore.Qt.SolidLine)
pen.setCosmetic(True)
pen.setCapStyle(QtCore.Qt.FlatCap)
l = self.scene.addLine(line, pen)
And once again, we can see how the margins are the same as if we used a non-cosmetic pen:

This isn't quite the answer but I thought it might help:
I did this in C++ but it's easy enough to translate. In your QGraphicsView, set the scrollbar policies:
view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
Then in your call to fitInView, add the following flag:
view->fitInView(scene->sceneRect(), Qt::KeepAspectRatioByExpanding);

Related

How to draw a figure on mouse click in qtapplication

I'm new to Pyqt5 and writing applications with it in Python so forgive me if this is a very simple question but I'm having trouble drawing ellipses in my program. I want to draw one by wherever a click occurs. Here is my code:
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import *
class Window(QGraphicsView):
def __init__(self):
super().__init__()
self.scene = QGraphicsScene()
self.setScene(self.scene)
# p.setTransform(transform)
self.button = QPushButton("Draw")
self.button.setCheckable(True)
self.button.setGeometry(0, 0, 100, 30)
self.scene.addWidget(self.button)
# self.setMouseTracking(True)
width, height = 1000, 1000
self.setFixedSize(width, height);
self.setSceneRect(0, 0, width, height);
self.fitInView(0, 0, width, height, Qt.KeepAspectRatio);
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.show()
def mousePressEvent(self, event):
if self.button.isChecked():
x = event.x()
y = event.y()
print(x, y)
ellipse = QGraphicsEllipseItem(x, y, 50, 20)
self.scene.addItem(ellipse)
The issue I'm having is I think the mousePressEvent function isn't allowing me to click on my button to enable drawing but the part I'm really not sure about is what is going on in the mousePressEvent. It seems as though it's getting the (x, y) coordinates within the QGraphicsView object but my ellipses are getting drawn in strange spots far away from wherever is clicked in my application when it's open.
You should not override the mousePressEvent as you remove the default behavior such as sending the event to the button. On the other hand you have to convert the coordinates of the view to the coordinates of the scene.
self.proxy_widget = self.scene.addWidget(self.button)
def mousePressEvent(self, event):
super().mousePressEvent(event)
vp = event.pos()
if self.proxy_widget in self.items(vp):
return
if self.button.isChecked():
ellipse = QGraphicsEllipseItem(0, 0, 50, 20)
self.scene.addItem(ellipse)
sp = self.mapToScene(vp)
ellipse.setPos(sp)

How to Draw (directly) on Screen in Windows with Python?

I want to create visual hints for users on their screens, but I got struck finding a simple solution how to do basic drawings straight on the screen without limiting the user actions (under Windows with Python 3.x).
After a search the only - not properly working - solution I found was using wxPython. Here's the code:
import wx
# init
app=wx.App()
dc=wx.ScreenDC()
# set line and fill style
dc.SetBrush(wx.TRANSPARENT_BRUSH)
dc.SetPen(wx.Pen((255, 0, 0), width=3, style=wx.PENSTYLE_SOLID))
# draw (x, y, width, height)
dc.DrawRectangle(100, 100, 200, 100)
The code draws on the screen, but the result barely becomes visible as the screen is redrawn (by Windows) very quick. I tried a work-around in repeating the drawing command with a for-loop, but also the flickering rectangle is barely visible (and this is nothing that I like to show to my clients).
A bit better (close to sufficient) is using a transparent TKinter window (without header) and display it for a - shorter - period of time. Here's the WORKING code of that (with one downside that is explained below the code):
from tkinter import *
def HighlightSection(Rect=(100,100,300,200), Color = 'red', Duration = 3):
win= Tk()
GeometryString = str(Rect[0])+'x'+str(Rect[1])+'+' \
+str(Rect[2])+'+'+str(Rect[3])
win.geometry(GeometryString) # "200x100+300+250" # breite, höhe, x, y #
win.configure(background=Color)
win.overrideredirect(1)
win.attributes('-alpha', 0.3)
win.wm_attributes('-topmost', 1)
win.after(Duration * 1000, lambda: win.destroy())
win.mainloop()
One thing here I could not make working: Any chance to make this TKinter "window" click-trough? Then this would be sufficient (close to quite good). As long as it is not click-trough the user cannot act in/under the highlighted area!
Is there a simple, solid solution to make draws (line, rectangles, text) on the screen and take it off again after a defined period of time? Any help is appreciated! Thanks in advance, Ulrich!
I believe drawing anything to the screen requires a window. The window can be partially transparent, but it needs to exist.
Something like this with PyQt5 may work for you. This will give you a transparent main window and draws a couple of lines:
import sys
from PyQt5.QtGui import QPainter, QPen
from PyQt5.QtWidgets import QMainWindow, QApplication
from PyQt5.QtCore import Qt
class Clear(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(300, 300, 280, 270)
self.setStyleSheet("background:transparent")
self.setAttribute(Qt.WA_TranslucentBackground)
self.show()
def paintEvent(self, e):
qp = QPainter()
qp.begin(self)
self.drawLines(qp)
qp.end()
def drawLines(self, qp):
pen = QPen(Qt.blue, 2, Qt.SolidLine)
qp.setPen(pen)
qp.drawLine(20, 40, 250, 40)
pen.setStyle(Qt.DashLine)
qp.setPen(pen)
qp.drawLine(20, 80, 250, 80)
pen.setStyle(Qt.DashDotLine)
qp.setPen(pen)
qp.drawLine(20, 120, 250, 120)
pen.setStyle(Qt.DotLine)
qp.setPen(pen)
qp.drawLine(20, 160, 250, 160)
pen.setStyle(Qt.DashDotDotLine)
qp.setPen(pen)
qp.drawLine(20, 200, 250, 200)
pen.setStyle(Qt.CustomDashLine)
pen.setDashPattern([1, 4, 5, 4])
qp.setPen(pen)
qp.drawLine(20, 240, 250, 240)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = Clear()
sys.exit(app.exec_())
Reference Source: http://zetcode.com/gui/pyqt5/painting/ - I modified it a bit to adjust the transparency.
This works, tested on macOS with Python 3.x.
from PyQt5.QtGui import (QPainter,
QPen,
QColor)
from PyQt5.QtWidgets import (QMainWindow,
QApplication)
from PyQt5.QtCore import (Qt,
QCoreApplication,
QTimer)
class TransparentWindow(QMainWindow):
def __init__(
self,
x: int,
y: int,
width: int,
height: int,
pen_color: str,
pen_size: int):
super().__init__()
self.highlight_x = x
self.highlight_y = y
self.highlight_width = width
self.highlight_height = height
self.pen_color = pen_color
self.pen_size = pen_size
self.initUI()
def initUI(self):
"""Initialize the user interface of the window."""
self.setGeometry(
self.highlight_x,
self.highlight_y,
self.highlight_width + self.pen_size,
self.highlight_height + self.pen_size)
self.setStyleSheet('background: transparent')
self.setWindowFlag(Qt.FramelessWindowHint)
def paintEvent(self, event):
"""Paint the user interface."""
painter = QPainter()
painter.begin(self)
painter.setPen(QPen(QColor(self.pen_color), self.pen_size))
painter.drawRect(
self.pen_size - 1,
self.pen_size - 1,
self.width() - 2 * self.pen_size,
self. height() - 2 * self.pen_size)
painter.end()
def highlight_on_screen(
x: int,
y: int,
width: int,
height: int,
pen_color: str = '#aaaa00',
pen_size: int = 2,
timeout: int = 2):
"""Highlights an area as a rectangle on the main screen.
`x` x position of the rectangle
`y` y position of the rectangle
`width` width of the rectangle
`height` height of the rectangle
`pen_color` Optional: color of the rectangle as a hex value;
defaults to `#aaaa00`
`pen_size` Optional: border size of the rectangle; defaults to 2
`timeout` Optional: time in seconds the rectangle
disappears; defaults to 2 seconds
"""
app = QApplication([])
window = TransparentWindow(x, y, width, height, pen_color, pen_size)
window.show()
QTimer.singleShot(timeout * 1000, QCoreApplication.quit)
app.exec_()
highlight_on_screen(0, 0, 100, 100)
Resulting rectangle on screen
However, on macOS it is not possible to draw a window over the app bar. That shouldn't be an issue on Windows.

Different colours in an arc

Consider the following toy example:
from PyQt5 import QtWidgets, QtGui, QtCore
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
w = QtWidgets.QWidget()
layout = QtWidgets.QVBoxLayout()
w.setLayout(layout)
self.setCentralWidget(w)
label = QtWidgets.QLabel()
canvas = QtGui.QPixmap(400, 300)
label.setPixmap(canvas)
layout.addWidget(label)
def paintEvent():
painter = QtGui.QPainter(label.pixmap())
painter.setRenderHint(QtGui.QPainter.Antialiasing)
painter.setPen(QtCore.Qt.red)
painter.drawArc(0, 0, 100, 100, 1440, -2880)
painter.end()
paintEvent()
self.show()
app = QtWidgets.QApplication([])
window = MainWindow()
app.exec_()
How can I paint the arc using an arbitrary number of colours ideally of varying lengths?
I tried to do it with gradients (linear and conical) but I have been unable to obtain accurate results.
I suppose the broader question is can I somehow have different pen colours when painting an arc? Note that the arc can be a half circle, a full circle or anything in between.
The colours are to be distributed using percentages. Each colour is a fraction of the arc's length. But I am content with a solution where all colours are equally spaced.
A possible solution is to paint the arc in parts:
from PyQt5 import QtCore, QtGui, QtWidgets
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
w = QtWidgets.QWidget()
layout = QtWidgets.QVBoxLayout()
w.setLayout(layout)
self.setCentralWidget(w)
label = QtWidgets.QLabel()
canvas = QtGui.QPixmap(400, 300)
canvas.fill(QtGui.QColor("white"))
label.setPixmap(canvas)
layout.addWidget(label)
def paint_label():
painter = QtGui.QPainter(label.pixmap())
painter.setRenderHint(QtGui.QPainter.Antialiasing)
r = QtCore.QRect(0, 0, 100, 100)
delta_angle = -180 * 16
start_angle = 90 * 16
values = (1, 2, 3, 4)
colors = (
QtGui.QColor("red"),
QtGui.QColor("blue"),
QtGui.QColor("green"),
QtGui.QColor("yellow"),
)
sum_of_values = sum(values)
for value, color in zip(values, colors):
end_angle = start_angle + int((value/sum_of_values) * delta_angle)
painter.setPen(color)
painter.drawArc(r, start_angle, end_angle - start_angle)
start_angle = end_angle
painter.end()
paint_label()
self.show()
def main():
app = QtWidgets.QApplication([])
window = MainWindow()
app.exec_()
if __name__ == "__main__":
main()
The solution provided by eyllanesc is perfectly fine, but I wanted to show the possibility of achieving the same result using a conical gradient instead of drawing single arcs.
Since we want actual arcs to be drawn, the trick is to use "ranges" of colors with very narrow margins.
For example, to get a conical gradient that is half red and half blue, we'll use something like this:
gradient.setColorAt(.5, QtCore.Qt.red)
# set the next color with a stop very close to the previous
gradient.setColorAt(.500001, QtCore.Qt.blue)
I prepared an example with a small interface to test its possibilities out.
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
w = QtWidgets.QWidget()
layout = QtWidgets.QVBoxLayout()
w.setLayout(layout)
self.setCentralWidget(w)
panelLayout = QtWidgets.QHBoxLayout()
layout.addLayout(panelLayout)
panelLayout.addWidget(QtWidgets.QLabel('Start'))
self.startSpin = QtWidgets.QSpinBox(maximum=360, suffix='°')
self.startSpin.setValue(90)
panelLayout.addWidget(self.startSpin)
panelLayout.addWidget(QtWidgets.QLabel('Extent'))
self.extentSpin = QtWidgets.QSpinBox(maximum=360, suffix='°')
self.extentSpin.setValue(180)
panelLayout.addWidget(self.extentSpin)
panelLayout.addWidget(QtWidgets.QLabel('Width'))
self.penSpin = QtWidgets.QSpinBox(minimum=1, maximum=20, suffix='px')
self.penSpin.setValue(3)
panelLayout.addWidget(self.penSpin)
self.startSpin.valueChanged.connect(self.updateCanvas)
self.extentSpin.valueChanged.connect(self.updateCanvas)
self.penSpin.valueChanged.connect(self.updateCanvas)
self.colors = []
self.colorSpins = []
colorLayout = QtWidgets.QHBoxLayout()
layout.addLayout(colorLayout)
for color in ('red', 'green', 'blue', 'yellow'):
colorLayout.addWidget(QtWidgets.QLabel(color))
self.colors.append(QtGui.QColor(color))
colorSpin = QtWidgets.QSpinBox(minimum=1, maximum=50, value=25)
colorLayout.addWidget(colorSpin)
colorSpin.valueChanged.connect(self.updateCanvas)
self.colorSpins.append(colorSpin)
self.label = QtWidgets.QLabel()
canvas = QtGui.QPixmap(400, 300)
self.label.setPixmap(canvas)
layout.addWidget(self.label)
self.updateCanvas()
self.show()
def updateCanvas(self):
pm = QtGui.QPixmap(self.label.pixmap().size())
pm.fill(QtCore.Qt.transparent)
painter = QtGui.QPainter(pm)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
painter.translate(.5, .5)
sizes = [spin.value() for spin in self.colorSpins]
total = sum(sizes)
extent = self.extentSpin.value() / 360
grad = QtGui.QConicalGradient(50, 50, self.startSpin.value())
gradPos = 1
# set colors starting from stop 1.0 to (1.0 - extent), since
# conical gradients are always counter-clockwise and the actual arc
# is negative, so it is drawn clockwise
for i, (size, color) in enumerate(zip(sizes, self.colors)):
grad.setColorAt(gradPos, color)
gradPos -= size / total * extent
if i < len(self.colors) - 1:
# extend the color right next to the next value
grad.setColorAt(gradPos + .000001, color)
if extent != 1:
# ensure that the first color is not painted at the edget of the
# last due to antialiasing
grad.setColorAt(0, self.colors[0])
grad.setColorAt(1 - extent, self.colors[-1])
offset = self.penSpin.maximum()
pen = QtGui.QPen(grad, self.penSpin.value(), cap=QtCore.Qt.FlatCap)
painter.setPen(pen)
# move the brush origin so that the conical gradient correctly centered
# in the middle of the ellipse
painter.setBrushOrigin(offset, offset)
painter.drawArc(offset, offset, 100, 100, self.startSpin.value() * 16, -self.extentSpin.value() * 16)
painter.end()
self.label.setPixmap(pm)

Draw 3D Rectangle/Polygon with Elevation and Lowering Effect in PyQt

In Java, you can draw 3-d rectangles using (see https://way2java.com/awt-graphics/4891/):
void fill3DRect(int x, int y, int width, int height, boolean raised)
Here, the last parameter "raised" is used to lower/elevate the 3d rectangle with respect to the drawing surface.
How can I achieve this effect in PyQt?
It depends on what level of paint you want to use:
There are 2 options:
Using QPainter:
This effect can be achieved by drawing 2 displaced rectangles where the color of the background rectangle is darker than the color of the front:
from PyQt5 import QtCore, QtGui, QtWidgets
def draw3DRect(painter, rect, color, raised=False, offset=QtCore.QPoint(4, 4)):
if raised:
painter.fillRect(rect.translated(offset), color.darker())
painter.fillRect(rect, color)
class Widget(QtWidgets.QWidget):
def paintEvent(self, event):
painter = QtGui.QPainter(self)
r = QtCore.QRect(
self.width() / 4,
self.height() / 4,
self.width() / 2,
self.height() / 2,
)
draw3DRect(painter, r, QtGui.QColor("green"), raised=True)
def sizeHint(self):
return QtCore.QSize(320, 240)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
Using QGraphicsDropShadowEffect:
In this case the QWidget and QGraphicsItem support this effect:
from PyQt5 import QtCore, QtGui, QtWidgets
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = QtWidgets.QWidget()
lay = QtWidgets.QHBoxLayout(w)
scene = QtWidgets.QGraphicsScene()
view = QtWidgets.QGraphicsView(scene)
rect_item = QtWidgets.QGraphicsRectItem(QtCore.QRectF(0, 0, 200, 100))
rect_item.setBrush(QtGui.QColor("green"))
effect_item = QtWidgets.QGraphicsDropShadowEffect(
offset=QtCore.QPointF(3, 3), blurRadius=5
)
rect_item.setGraphicsEffect(effect_item)
scene.addItem(rect_item)
rect_widget = QtWidgets.QWidget()
rect_widget.setFixedSize(320, 240)
rect_widget.setStyleSheet("background-color:green;")
effect_widget = QtWidgets.QGraphicsDropShadowEffect(
offset=QtCore.QPointF(3, 3), blurRadius=5
)
rect_widget.setGraphicsEffect(effect_widget)
lay.addWidget(view)
lay.addWidget(rect_widget)
w.resize(640, 480)
w.show()
sys.exit(app.exec_())
To my knowledge there is no built in PyQt 3D paint widget/function as you can only paint 2D polygons. But we can create a custom class to emulate 3D painting. From your Java linked reference:
Java supports 3D rectangles but the effect of third dimension is not very visible. As the elevation is less, the effect is negligible. Java designers gave the effect of 3D by drawing lighter and darker lines along the rectangle border.
We can emulate the effect of Java's 3D paint function:
void fill3DRect(int x, int y, int width, int height, boolean raised)
This method draws a solid 3D rectangle with the above specified parameters. The last boolean parameter true indicates elevation above the drawing surface and false indicates etching into the surface.
To obtain a 3D effect in Python we can essentially do the same thing by having two shades of a color then darkening and lighting some sides.
from PyQt5 import QtCore, QtGui, QtWidgets
import sys
class Rectangle3D(QtWidgets.QWidget):
def __init__(self, parent=None):
QtWidgets.QWidget.__init__(self, parent)
# Elevated 3D rectangle color settings
self.elevated_border_color = QtGui.QColor(111,211,111)
self.elevated_fill_color = QtGui.QColor(0,255,0)
self.elevated_pen_width = 2.5
# Lowered 3D rectangle color settings
self.lowered_border_color = QtGui.QColor(0,235,0)
self.lowered_fill_color = QtGui.QColor(0,178,0)
self.lowered_pen_width = 2.5
def draw3DRectangle(self, x, y, w, h, raised=True):
# Specify the border/fill colors depending on raised or lowered
if raised:
# Line color (border)
self.pen = QtGui.QPen(self.elevated_border_color, self.elevated_pen_width)
# Fill color
self.fill = QtGui.QBrush(self.elevated_fill_color)
else:
# Line color (border)
self.pen = QtGui.QPen(self.lowered_border_color, self.lowered_pen_width)
# Fill color
self.fill = QtGui.QBrush(self.lowered_fill_color)
painter = QtGui.QPainter(self)
# Draw border color of rectangle
painter.setPen(self.pen)
painter.setBrush(self.fill)
painter.drawRect(x, y, w, h)
# Cover up the top and left sides with filled color using lines
if raised:
painter.setPen(QtGui.QPen(self.elevated_fill_color, self.elevated_pen_width))
else:
painter.setPen(QtGui.QPen(self.lowered_fill_color, self.lowered_pen_width))
painter.drawLine(x, y, x + w, y)
painter.drawLine(x, y, x, y + h)
def paintEvent(self, event):
self.draw3DRectangle(50,50,300,150,True)
self.draw3DRectangle(50,250,300,150,False)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
widget = Rectangle3D()
widget.show()
sys.exit(app.exec_())

Painting on a widget that contains a QGridLayout in PySide/PyQt

I am making a custom QWidget in which I have a QGridLayout, and draw a rectangle on a particular element in the grid. I also manually draw lines to delineate the location of the grid elements (with QPainter.DrawLines).
After drawing the lines, I then paint the rectangle within one of the grid elements, with its location specified using the QGridLayout coordinate system .
The problem is, the rectangle does not stay confined to its grid element. For instance, in the example below, the blue rectangle and black grid lines get out of alignment, so I end up with a blue box floating around in space.
I have not found explicit discussion of this issue via Google or SO.
Edit:
Note as pointed out in the accepted answer, the mistake was using grid coordinates to draw on the grid, when I should have been using point coordinates (i.e., column, row). That is, the mistake in the code below is that the element in the grid has its x- and y- coordinates reversed.
from PySide import QtGui, QtCore
class HighlightSquare(QtGui.QWidget):
def __init__(self, parent = None):
QtGui.QWidget.__init__(self, parent=None)
self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding,
QtGui.QSizePolicy.Expanding))
self.setMinimumSize(self.minimumSizeHint())
layout = QtGui.QGridLayout()
layout.addItem(QtGui.QSpacerItem(10,10), 0, 0)
layout.addItem(QtGui.QSpacerItem(10,10), 0, 1)
layout.addItem(QtGui.QSpacerItem(10,10), 1, 0)
layout.addItem(QtGui.QSpacerItem(10,10), 1, 1)
self.setLayout(layout)
self.resize(150, 150)
self.update()
def paintEvent(self, event = None):
painter = QtGui.QPainter(self)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
winHeight=self.size().height(); heightStep=winHeight/2
winWidth=self.size().width(); widthStep=winWidth/2
#Draw lines
painter.setPen(QtCore.Qt.black)
for i in range(4):
#vertical lines
painter.drawLine(QtCore.QPoint(i*widthStep,0), QtCore.QPoint(i*widthStep, winHeight))
#horizontal lines
painter.drawLine(QtCore.QPoint(0,heightStep*i), QtCore.QPoint(winWidth, heightStep*i))
#Draw blue outline around box 1,1
highlightCoordinate=(1,1)
pen=QtGui.QPen(QtCore.Qt.blue, 3)
painter.setPen(pen)
coordHighlight=[QtCore.QPoint(highlightCoordinate[1]*heightStep, highlightCoordinate[0]*widthStep),\
QtCore.QPoint(highlightCoordinate[1]*heightStep, (highlightCoordinate[0]+1)*widthStep),\
QtCore.QPoint((highlightCoordinate[1]+1)*heightStep, (highlightCoordinate[0]+1)*widthStep),\
QtCore.QPoint((highlightCoordinate[1]+1)*heightStep, highlightCoordinate[0]*widthStep),\
QtCore.QPoint(highlightCoordinate[1]*heightStep, highlightCoordinate[0]*widthStep)]
#print coordHighlight
painter.drawPolyline(coordHighlight)
def minimumSizeHint(self):
return QtCore.QSize(120,120)
if __name__=="__main__":
import sys
app=QtGui.QApplication(sys.argv)
myLight = HighlightSquare()
myLight.show()
sys.exit(app.exec_())
Have you read the definition of the constructor of class QtCore.QPoint? At method QPoint.__init__ (self, int xpos, int ypos) your code is reversed (ypos, xpos). I fixed it.
import sys
from PyQt4 import QtGui, QtCore
class QHighlightSquareWidget (QtGui.QWidget):
def __init__ (self, parent = None):
QtGui.QWidget.__init__(self, parent = None)
self.setSizePolicy (
QtGui.QSizePolicy (
QtGui.QSizePolicy.Expanding,
QtGui.QSizePolicy.Expanding))
self.setMinimumSize(self.minimumSizeHint())
allQGridLayout = QtGui.QGridLayout()
allQGridLayout.addItem(QtGui.QSpacerItem(10,10), 0, 0)
allQGridLayout.addItem(QtGui.QSpacerItem(10,10), 0, 1)
allQGridLayout.addItem(QtGui.QSpacerItem(10,10), 1, 0)
allQGridLayout.addItem(QtGui.QSpacerItem(10,10), 1, 1)
self.setLayout(allQGridLayout)
self.resize(150, 150)
self.update()
def paintEvent (self, eventQPaintEvent):
myQPainter = QtGui.QPainter(self)
myQPainter.setRenderHint(QtGui.QPainter.Antialiasing)
winHeight = self.size().height()
heightStep = winHeight / 2
winWidth = self.size().width()
widthStep = winWidth / 2
myQPainter.setPen(QtCore.Qt.black)
for i in range(4):
myQPainter.drawLine(QtCore.QPoint(i * widthStep, 0 ), QtCore.QPoint(i * widthStep, winHeight ))
myQPainter.drawLine(QtCore.QPoint(0, heightStep * i), QtCore.QPoint(winWidth, heightStep * i))
highlightCoordinate = (1, 1)
myQPen = QtGui.QPen(QtCore.Qt.blue, 3)
myQPainter.setPen(myQPen)
coordHighlight = [
QtCore.QPoint( highlightCoordinate[0] * widthStep, highlightCoordinate[1] * heightStep),
QtCore.QPoint((highlightCoordinate[0] + 1) * widthStep, highlightCoordinate[1] * heightStep),
QtCore.QPoint((highlightCoordinate[0] + 1) * widthStep, (highlightCoordinate[1] + 1) * heightStep),
QtCore.QPoint( highlightCoordinate[0] * widthStep, (highlightCoordinate[1] + 1) * heightStep),
QtCore.QPoint( highlightCoordinate[0] * widthStep, highlightCoordinate[1] * heightStep)]
myQPainter.drawPolyline(*coordHighlight)
def minimumSizeHint (self):
return QtCore.QSize(120, 120)
if __name__=="__main__":
myQApplication = QtGui.QApplication(sys.argv)
myQHighlightSquareWidget = QHighlightSquareWidget()
myQHighlightSquareWidget.show()
sys.exit(myQApplication.exec_())

Categories