PyOpengGL raises error when I compile shaders - python

I am running through a cumulation of OpenGL shader tutorials and mixing and matching stuff, trying to get a custom shader implemented. I have the following Python code and traceback:
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QOpenGLWidget
from PyQt5.QtCore import Qt
from OpenGL.GL import (
glLoadIdentity, glTranslatef, glRotatef,
glClear, glBegin, glEnd,
glColor3fv, glVertex3fv,
GL_COLOR_BUFFER_BIT, GL_DEPTH_BUFFER_BIT,
GL_QUADS, GL_LINES,
shaders, GL_VERTEX_SHADER, GL_FRAGMENT_SHADER
)
from OpenGL.GLU import gluPerspective
class mainWindow(QMainWindow): #Main class.
def keyPressEvent(self, event): #This is the keypress detector.
try:
key = event.key()
except:
key = -1
#print(key)
if key == 16777216:
exit()
vertices = [
(-1, 1, 0),
(1, 1, 0),
(1, -1, 0),
(-1, -1, 0)
]
wires = [
(0, 1),
(1, 2),
(2, 3),
(0, 3)
]
facets = [
(0, 1, 2, 3)
]
zoomLevel = -5
rotateDegreeH = 0
rotateDegreeV = -45
vertShaderCode = """#version 120
void main() {
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}"""
fragShaderCode = """#version 120
void main() {
gl_FragColor = vec4( 0, 1, 0, 1 );
}"""
def __init__(self):
super(mainWindow, self).__init__()
self.sizeX = 700 #Variables used for the setting of the size of everything
self.sizeY = 600
self.setGeometry(0, 0, self.sizeX + 50, self.sizeY) #Set the window size
#make shaders
VERTEX_SHADER = shaders.compileShader(self.vertShaderCode, GL_VERTEX_SHADER)
FRAGMENT_SHADER = shaders.compileShader(self.fragShaderCode, GL_FRAGMENT_SHADER)
self.shader = shaders.compileProgram(VERTEX_SHADER,FRAGMENT_SHADER)
self.openGLWidget = QOpenGLWidget(self) #Create the GLWidget
self.openGLWidget.setGeometry(0, 0, self.sizeX, self.sizeY)
self.openGLWidget.initializeGL()
self.openGLWidget.resizeGL(self.sizeX, self.sizeY) #Resize GL's knowledge of the window to match the physical size?
self.openGLWidget.paintGL = self.paintGL #override the default function with my own?
def nav(self, hVal = 0, vVal = 0, zVal = 0):
self.zoomLevel += zVal
self.rotateDegreeH += hVal
self.rotateDegreeV += vVal
self.openGLWidget.update()
def paintGL(self):
#This function uses shape objects, such as cube() or mesh(). Shape objects require the following:
#a list named 'vertices' - This list is a list of points, from which edges and faces are drawn.
#a list named 'wires' - This list is a list of tuples which refer to vertices, dictating where to draw wires.
#a list named 'facets' - This list is a list of tuples which refer to vertices, ditating where to draw facets.
#a bool named 'render' - This bool is used to dictate whether or not to draw the shape.
#a bool named 'drawWires' - This bool is used to dictate whether wires should be drawn.
#a bool named 'drawFaces' - This bool is used to dictate whether facets should be drawn.
shaders.glUseProgram(self.shader)
glLoadIdentity()
gluPerspective(45, self.sizeX / self.sizeY, 0.1, 110.0) #set perspective?
glTranslatef(0, 0, self.zoomLevel) #I used -10 instead of -2 in the PyGame version.
glRotatef(self.rotateDegreeV, 1, 0, 0) #I used 2 instead of 1 in the PyGame version.
glRotatef(self.rotateDegreeH, 0, 0, 1)
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
glBegin(GL_LINES)
for w in self.wires:
for v in w:
glVertex3fv(self.vertices[v])
glEnd()
glBegin(GL_QUADS)
for f in self.facets:
for v in f:
glVertex3fv(self.vertices[v])
glEnd()
app = QApplication([])
window = mainWindow()
window.show()
sys.exit(app.exec_())
C:\Users\ccronk22\Documents\Python\glShaders>pygl002.py
Traceback (most recent call last):
File "C:\Users\ccronk22\AppData\Local\Programs\Python\Python38\lib\site-packages\OpenGL\latebind.py", line 43, in __call__
return self._finalCall( *args, **named )
TypeError: 'NoneType' object is not callable
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "C:\Users\ccronk22\Documents\Python\glShaders\pyGL002.py", line 109, in <module>
window = mainWindow()
File "C:\Users\ccronk22\Documents\Python\glShaders\pyGL002.py", line 59, in __init__
VERTEX_SHADER = shaders.compileShader(self.vertShaderCode, GL_VERTEX_SHADER)
File "C:\Users\ccronk22\AppData\Local\Programs\Python\Python38\lib\site-packages\OpenGL\GL\shaders.py", line 228, in compileShader
shader = glCreateShader(shaderType)
File "C:\Users\ccronk22\AppData\Local\Programs\Python\Python38\lib\site-packages\OpenGL\latebind.py", line 46, in __call__
self._finalCall = self.finalise()
File "C:\Users\ccronk22\AppData\Local\Programs\Python\Python38\lib\site-packages\OpenGL\extensions.py", line 242, in finalise
raise error.NullFunctionError(
OpenGL.error.NullFunctionError: Attempt to call an undefined alternate function (glCreateShader, glCreateShaderObjectARB), check for bool(glCreateShader) before calling
C:\Users\ccronk22\Documents\Python\glShaders>
The above code works well if you comment out the compileShader, compileProgram, and useProgram lines. It produces a white square, viewed from an above angle:
def __init__(self):
super(mainWindow, self).__init__()
self.sizeX = 700 #Variables used for the setting of the size of everything
self.sizeY = 600
self.setGeometry(0, 0, self.sizeX + 50, self.sizeY) #Set the window size
#make shaders
#VERTEX_SHADER = shaders.compileShader(self.vertShaderCode, GL_VERTEX_SHADER)
#FRAGMENT_SHADER = shaders.compileShader(self.fragShaderCode, GL_FRAGMENT_SHADER)
#self.shader = shaders.compileProgram(VERTEX_SHADER,FRAGMENT_SHADER)
[…]
def paintGL(self):
#This function uses shape objects, such as cube() or mesh(). Shape objects require the following:
#a list named 'vertices' - This list is a list of points, from which edges and faces are drawn.
#a list named 'wires' - This list is a list of tuples which refer to vertices, dictating where to draw wires.
#a list named 'facets' - This list is a list of tuples which refer to vertices, ditating where to draw facets.
#a bool named 'render' - This bool is used to dictate whether or not to draw the shape.
#a bool named 'drawWires' - This bool is used to dictate whether wires should be drawn.
#a bool named 'drawFaces' - This bool is used to dictate whether facets should be drawn.
#shaders.glUseProgram(self.shader)
Some additional information:
I am attempting to make the code from http://pyopengl.sourceforge.net/context/tutorials/shader_1.html work in the PyQt5 context, with only the OpenGL and PyQt5 APIs. The tutorial was written in Python2. I am using Python3.
So, what am I doing wrong with my shader compilation? Is it in the Python or GLSL?

To compile and link the shader you need a valid and current OpenGL Context. Note the OpenGL context have to be current when any OpenGL instruction is invoked.
QOpenGLWidget provides the virtual methods
def initializeGL ()
def paintGL ()
def resizeGL (w, h)
where the OpenGL context is active. This methods are callbacks and invoked by Qts event handling. Don't call them in your code.
Create the shader in the initializeGL callback:
class mainWindow(QMainWindow):
def __init__(self):
super(mainWindow, self).__init__()
self.sizeX = 700 #Variables used for the setting of the size of everything
self.sizeY = 600
self.setGeometry(0, 0, self.sizeX + 50, self.sizeY) #Set the window size
self.openGLWidget = QOpenGLWidget(self) #Create the GLWidget
self.openGLWidget.setGeometry(0, 0, self.sizeX, self.sizeY)
self.openGLWidget.resizeGL(self.sizeX, self.sizeY) #Resize GL's knowledge of the window to match the physical size?
self.openGLWidget.initializeGL = self.initializeGL
self.openGLWidget.paintGL = self.paintGL #override the default function with my own?
self.shader = None
def nav(self, hVal = 0, vVal = 0, zVal = 0):
self.zoomLevel += zVal
self.rotateDegreeH += hVal
self.rotateDegreeV += vVal
self.openGLWidget.update()
def initializeGL(self):
#make shaders
VERTEX_SHADER = shaders.compileShader(self.vertShaderCode, GL_VERTEX_SHADER)
FRAGMENT_SHADER = shaders.compileShader(self.fragShaderCode, GL_FRAGMENT_SHADER)
self.shader = shaders.compileProgram(VERTEX_SHADER,FRAGMENT_SHADER)
def paintGL(self):
glUseProgram(self.shader)
# [...]
(The import of glUseProgram is missing in your code)

Related

Drawing an OpenGL triangle in Python

Here is my code so far:
main.py_
from Application import Application
if __name__ == '__main__':
app = Application()
app.run()
Triangle.py_
import contextlib
import logging as log
from OpenGL import GL as gl
import ctypes
import sys
class Triangle:
vertex_array_id = 0
vertex_data = []
program_id = 0
shader_id = 0
def __init__(self):
print('Triangle.__init__(self)')
self.create_vertex_buffer()
self.load_shaders()
def create_vertex_array_object(self):
log.debug('create_vertex_array_object(self):')
self.vertex_array_id = gl.glGenVertexArrays(1)
try:
gl.glBindVertexArray(self.vertex_array_id)
yield
finally:
log.debug('~create_vertex_array_object(self):')
gl.glDeleteVertexArrays(1, [self.vertex_array_id])
def create_vertex_buffer(self):
with self.create_vertex_array_object():
# A triangle
self.vertex_data = [-1, -1, 0,
1, -1, 0,
0, 1, 0]
attr_id = 0 # No particular reason for 0,
# but must match the layout location in the shader.
log.debug('creating and binding the vertex buffer (VBO)')
vertex_buffer = gl.glGenBuffers(1)
try:
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, vertex_buffer)
array_type = (gl.GLfloat * len(self.vertex_data))
gl.glBufferData(gl.GL_ARRAY_BUFFER,
len(self.vertex_data) * ctypes.sizeof(ctypes.c_float),
array_type(*self.vertex_data),
gl.GL_STATIC_DRAW)
log.debug('setting the vertex attributes')
gl.glVertexAttribPointer(
attr_id, # attribute 0.
3, # components per vertex attribute
gl.GL_FLOAT, # type
False, # to be normalized?
0, # stride
None # array buffer offset
)
gl.glEnableVertexAttribArray(attr_id) # use currently bound VAO
yield
finally:
log.debug('cleaning up buffer')
# gl.glDisableVertexAttribArray(attr_id)
# gl.glDeleteBuffers(1, [vertex_buffer])
def load_shaders(self):
shaders = {
gl.GL_VERTEX_SHADER: '''\
#version 330 core
layout(location = 0) in vec3 vertexPosition_modelspace;
void main(){
gl_Position.xyz = vertexPosition_modelspace;
gl_Position.w = 1.0;
}
''',
gl.GL_FRAGMENT_SHADER: '''\
#version 330 core
out vec3 color;
void main(){
color = vec3(1,0,0);
}
'''
}
log.debug('creating the shader program')
self.program_id = gl.glCreateProgram()
try:
shader_ids = []
for shader_type, shader_src in shaders.items():
self.shader_id = gl.glCreateShader(shader_type)
gl.glShaderSource(self.shader_id, shader_src)
log.debug(f'compiling the {shader_type} shader')
gl.glCompileShader(self.shader_id)
# check if compilation was successful
result = gl.glGetShaderiv(self.shader_id, gl.GL_COMPILE_STATUS)
info_log_len = gl.glGetShaderiv(self.shader_id, gl.GL_INFO_LOG_LENGTH)
if info_log_len:
logmsg = gl.glGetShaderInfoLog(self.shader_id)
log.error(logmsg)
sys.exit(10)
gl.glAttachShader(self.program_id, self.shader_id)
shader_ids.append(self.shader_id)
log.debug('linking shader program')
gl.glLinkProgram(self.program_id)
# check if linking was successful
result = gl.glGetProgramiv(self.program_id, gl.GL_LINK_STATUS)
info_log_len = gl.glGetProgramiv(self.program_id, gl.GL_INFO_LOG_LENGTH)
if info_log_len:
logmsg = gl.glGetProgramInfoLog(self.program_id)
log.error(logmsg)
sys.exit(11)
log.debug('installing shader program into rendering state')
gl.glUseProgram(self.program_id)
yield
finally:
log.debug('cleaning up shader program')
for self.shader_id in self.shader_ids:
gl.glDetachShader(self.program_id, self.shader_id)
gl.glDeleteShader(self.shader_id)
gl.glUseProgram(0)
gl.glDeleteProgram(self.program_id)
def render(self):
gl.glDrawArrays(gl.GL_TRIANGLES, 0, 3) # Starting from vertex 0
Application.py_
import contextlib
import glfw
import sys
from OpenGL import GL as gl
import logging as log
from Triangle import Triangle
class Application:
tri = Triangle()
def __init__(self):
print("Application.__init__(self)")
#contextlib.contextmanager
def create_main_window(self):
log.debug('create_main_window(self):')
if not glfw.init():
sys.exit(1)
try:
glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 3)
glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 3)
glfw.window_hint(glfw.OPENGL_FORWARD_COMPAT, True)
glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE)
title = 'Tutorial 2: First Triangle'
window = glfw.create_window(500, 400, title, None, None)
if not window:
sys.exit(2)
glfw.make_context_current(window)
gl.glClearColor(0, 0, 0.4, 0)
yield window
finally:
log.debug('~create_main_window(self):')
glfw.terminate()
def main_loop(self, window):
while (
glfw.get_key(window, glfw.KEY_ESCAPE) != glfw.PRESS and
not glfw.window_should_close(window)
):
gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
self.tri.render()
glfw.swap_buffers(window)
glfw.poll_events()
def run(self):
log.basicConfig(level=log.DEBUG)
with self.create_main_window() as window:
self.main_loop(window)
From the console I have this coming out:
DEBUG:root:create_main_window(self):
Triangle.__init__(self)
Application.__init__(self)
DEBUG:root:~create_main_window(self):
Traceback (most recent call last):
File "C:\Users\prussos\PycharmProjects\pythonProject3\main.py", line 5, in <module>
app.run()
File "C:\Users\prussos\PycharmProjects\pythonProject3\Application.py", line 51, in run
self.main_loop(window)
File "C:\Users\prussos\PycharmProjects\pythonProject3\Application.py", line 44, in main_loop
self.tri.render()
File "C:\Users\prussos\PycharmProjects\pythonProject3\Triangle.py", line 131, in render
gl.glDrawArrays(gl.GL_TRIANGLES, 0, 3) # Starting from vertex 0
File "C:\Users\prussos\PycharmProjects\pythonProject3\venv\lib\site-packages\OpenGL\platform\baseplatform.py", line 415, in __call__
return self( *args, **named )
File "C:\Users\prussos\PycharmProjects\pythonProject3\venv\lib\site-packages\OpenGL\error.py", line 230, in glCheckError
raise self._errorClass(
OpenGL.error.GLError: GLError(
err = 1282,
description = b'invalid operation',
baseOperation = glDrawArrays,
cArguments = (GL_TRIANGLES, 0, 3)
)
Process finished with exit code 1
There seems to be a drawing error that is happening but I really I not quite exactly sure how to start to troubleshoot this thing yet. The code was moved over from gitlabs into a class based system for python. So a draw arrays that is an invalid operation what could that be coming from in OpenGL library is anyone familiar with it enough to know what place to edit here?
When you call an OpenGL instruction, you need a valid and up-to-date OpenGL context. The OpenGL Context is created with the OpenGl window. The OpenGL objects (VAO, VBO and shaders) are create in the constructor of the Triangle class. Therefore you can construct an object of this class only after creating the OpenGL window:
class Application:
# tri = Triangle() # <-- DELETE
def __init__(self):
print("Application.__init__(self)")
#contextlib.contextmanager
def create_main_window(self):
log.debug('create_main_window(self):')
if not glfw.init():
sys.exit(1)
try:
glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 3)
glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 3)
glfw.window_hint(glfw.OPENGL_FORWARD_COMPAT, True)
glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE)
title = 'Tutorial 2: First Triangle'
window = glfw.create_window(500, 400, title, None, None)
if not window:
sys.exit(2)
glfw.make_context_current(window)
gl.glClearColor(0, 0, 0.4, 0)
yield window
finally:
log.debug('~create_main_window(self):')
glfw.terminate()
def main_loop(self, window):
self.tri = Triangle() # IINSERT
while (
glfw.get_key(window, glfw.KEY_ESCAPE) != glfw.PRESS and
not glfw.window_should_close(window)
):
gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
self.tri.render()
glfw.swap_buffers(window)
glfw.poll_events()
def run(self):
log.basicConfig(level=log.DEBUG)
with self.create_main_window() as window:
self.main_loop(window)

Catch events with QRectF

I have a QGraphicsScene where I add QRectF objects anchored to QWidget objects in order to move them. I'd need to capture events or signals from the QRectF but the mousePressEvent method never runs.
These objects have a sort of balance and it would be hard to replace the QRectF with a QRect or a QGraphicsRectItem, because drawing the base rect in the scene only that class is accepted.
I also tried to implement the mousePressEvent method is GraphicBlock class (which is a QWidget) but nothing happens.
This is my QRectF
class BlockRect(QRectF):
def __init__(self, x, y, dim1, dim2, block_type):
super(QRectF, self).__init__(x, y, dim1, dim2)
self.block_type = block_type
def contains(self, point):
if self.x() + self.width() \
> point.x() > self.x() - self.width()/2:
if self.y() + self.height() \
> point.y() > self.y() - self.height()/2:
return True
return False
# Never called
def mousePressEvent(self, event):
print("click!")
dialog = MyDialog(self.block_type)
dialog.exec()
super(BlockRect, self).mouseDoubleClickEvent(event)
And this is the method where I draw it:
def draw_block(self, block_type):
"""Drawing a graphic clock with its properties"""
# Setting the visible scene
viewport_rect = QRect(0, 0, self.view.viewport().width(),
self.view.viewport().height())
viewport = self.view.mapToScene(viewport_rect).boundingRect()
start_x = viewport.x()
start_y = viewport.y()
# The initial point of each block is translated of 20px in order not to
# overlap them (always in the visible area)
point = QPoint(start_x + 20*(self.numBlocks % 20) + 20,
start_y + 20*(self.numBlocks % 20) + 20)
transparent = QColor(0, 0, 0, 0)
# Creation of the graphic block
block = GraphicBlock(self.numBlocks, block_type, 0, self.scene)
# Positioning the block
proxy = self.scene.addWidget(block)
proxy.setPos(point.x(), point.y())
# Creation of the rect that will be parent of the QWidget GraphicBlock
# in order to move it in the QGraphicsScene
rect = BlockRect(point.x() + 10, point.y() + 10,
block.width() - 20, block.height() - 20,
block_type)
# The rect is added to the scene and becomes the block's parent
proxy_control = self.scene.addRect(rect, QPen(transparent), QBrush(transparent))
proxy_control.setFlag(QGraphicsItem.ItemIsMovable, True)
proxy_control.setFlag(QGraphicsItem.ItemIsSelectable, True)
proxy.setParentItem(proxy_control)
block.set_rect(rect)
self.blocks[self.numBlocks] = block
self.numBlocks += 1
self.update()
I really don't know or understand how i could capture events in some way.
Here it is my QWidget class, i.e. GraphicBlock, which do have event methods but doesn't execute them. I think I should control events from the QGraphicsScene object.
class GraphicBlock(QWidget):
"""QWidget that carries both graphical and logical information about the
layer node
"""
def __init__(self, block_id, block_type, block_data, scene):
super(GraphicBlock, self).__init__()
self.block_id = block_id
self.block_type = block_type
self.block_data = block_data # Just to try
self.scene = scene
self.rect = None
# Setting style and transparent background for the rounded corners
self.setAttribute(Qt.WA_TranslucentBackground)
self.setStyleSheet(GRAPHIC_BLOCK_STYLE)
# Block title label
type_label = QLabel(block_type.name)
type_label.setStyleSheet(BLOCK_TITLE_STYLE)
# Main vertical layout: it contains the label title and grid
layout = QVBoxLayout()
layout.setSpacing(0)
layout.addWidget(type_label)
self.setLayout(layout)
if block_type.parameters:
# Creating the grid for parameters
grid = QWidget()
grid_layout = QGridLayout()
grid.setLayout(grid_layout)
layout.addWidget(grid)
# Iterating and displaying parameters
par_labels = dict()
count = 1
for par in block_type.parameters:
par_labels[par] = QLabel(par)
par_labels[par].setAlignment(Qt.AlignLeft)
par_labels[par].setStyleSheet(PAR_BLOCK_STYLE)
dim = QLabel("<dim>")
dim.setAlignment(Qt.AlignRight)
dim.setStyleSheet(DIM_BLOCK_STYLE)
grid_layout.addWidget(par_labels[par], count, 1)
grid_layout.addWidget(dim, count, 0)
count += 1
else:
type_label.setStyleSheet(ZERO_PARS_BLOCK_TITLE)
def set_rect(self, rect):
self.rect = rect

why does my window doesn't work for on_draw?

I was watching a video about pyglet and I tried to create a triangle:
import pyglet
from pyglet.gl import *
class mywindow(pyglet.window.Window):
def __init__(self, *args,**kwargs):
super().__init__(*args,**kwargs)
self.set_minimum_size(300,300)
window = mywindow(300,300,"deneme", True)
def on_draw():
glBegin(GL_TRIANGLE)
glColor3b(255,0,0)
glVertex2f(-1,0)
glColor3b(0,255,0)
glVertex2f(1,0)
glColor3b(0,0,255)
glVertex2f(0,1)
window.on_draw()
pyglet.app.run()
when I run this code; I get this error:
AttributeError: 'mywindow' object has no attribute 'on_draw'
Any idea how to solve this?
on_draw has to be a method of the class mywindow rather than a function. Don't invoke on_draw yourself, because it is called automatically, when the window is needed to be updated.
At the begin of on_draw you've to clear the display by (see Windowing).
A OpenGL immediate mode glBegin/glEnd sequence has to be ended by glEnd. The primitive type is GL_TRIANGLES rather than GL_TRIANGLE. If you want to specify the colors in range [0, 255], the you've to use glColor3ub (unsigned byte) rather then glColor3b (signed byte).
You have to set the viewport rectangle of the resizable window by glViewport in the on_resize event.
See the example:
import pyglet
from pyglet.gl import *
class mywindow(pyglet.window.Window):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_minimum_size(300,300)
def on_draw(self):
self.clear()
glBegin(GL_TRIANGLES)
glColor3ub(255, 0, 0)
glVertex2f(-1, 0)
glColor3ub(0, 255, 0)
glVertex2f(1, 0)
glColor3ub(0, 0, 255)
glVertex2f(0, 1)
glEnd()
def on_resize(self, width, height):
glViewport(0, 0, width, height)
window = mywindow(300,300,"deneme", True)
pyglet.app.run()
In [1]: from pyglet.gl import *
...:
...: window = pyglet.window.Window()
...:
...: vlist = pyglet.graphics.vertex_list(3, ('v2f', [0,0, 400,50, 200,300]))
...:
...: #window.event
...: def on_draw():
...: glClear(pyglet.gl.GL_COLOR_BUFFER_BIT)
...: glColor3f(1,0,0)
...: vlist.draw(GL_TRIANGLES)
...:
...: pyglet.app.run()
Output:

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

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);

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