Anchoring widgets in kivy 1.9.0 RelativeLayout - python

I have created a RelativeLayout subclass that positions its children in a grid by supplying them positions in the code (Python, not kv file). It works, but items are placed some 25 pixels to the upper-right from the position of layout itself, as shown by the canvas block. Python code for Layout subclass:
class RLMapWidget(RelativeLayout):
def __init__(self, map=None, **kwargs):
super(FloatLayout, self).__init__(**kwargs)
# Connecting to map, factories and other objects this class should know about
self.tile_factory = TileWidgetFactory()
self.map = map
# Initializing tile widgets for BG layer and adding them as children
for x in range(self.map.size[0]):
for y in range(self.map.size[1]):
tile_widget = self.tile_factory.create_tile_widget(self.map.get_item(layer='bg',
location=(x, y)))
# tile_widget.pos = (50*x, 50*y)
tile_widget.pos = self._get_screen_pos((x, y))
self.add_widget(tile_widget)
# Initializing widgets for actor layers
for x in range(self.map.size[0]):
for y in range(self.map.size[1]):
if self.map.has_item(layer='actors', location=(x, y)):
actor_widget = self.tile_factory.create_actor_widget(self.map.get_item(layer='actors',
displayed location=(x, y)))
actor_widget.pos=(50*x, 50*y)
self.add_widget(actor_widget)
# Map background canvas. Used solely to test positioning
with self.canvas.before:
Color(0, 0, 1, 1)
self.rect = Rectangle(size = self.size, pos=self.pos)
self.bind(pos=self.update_rect, size=self.update_rect)
# Initializing keyboard bindings and key lists
self._keyboard = Window.request_keyboard(self._keyboard_closed, self)
self._keyboard.bind(on_key_down=self._on_key_down)
# The list of keys that will not be ignored by on_key_down
self.used_keys=['w', 'a', 's', 'd']
def redraw_actors(self):
for actor in self.map.actors:
actor.widget.pos = self._get_screen_pos(actor.location)
def _get_screen_pos(self, location):
"""
Return screen coordinates (in pixels) for a given location
:param location: int tuple
:return: int tuple
"""
return (location[0]*50, location[1]*50)
# Keyboard-related methods
def _on_key_down(self, keyboard, keycode, text, modifiers):
"""
Process keyboard event and make a turn, if necessary
:param keyboard:
:param keycode:
:param text:
:param modifiers:
:return:
"""
if keycode[1] in self.used_keys:
self.map.process_turn(keycode)
self.redraw_actors()
def _keyboard_closed(self):
self._keyboard.unbind(on_key_down=self._on_key_down)
self._keyboard = None
def update_rect(self, pos, size):
self.rect.pos = self.pos
self.rect.size = self.size
class CampApp(App):
def build(self):
root = FloatLayout()
map_factory = MapFactory()
map = map_factory.create_test_map()
map_widget = RLMapWidget(map=map,
size=(map.size[0]*50, map.size[1]*50),
size_hint=(None, None))
root.add_widget(map_widget)
return root
if __name__ == '__main__':
CampApp().run()
Factory class that makes tiles:
class TileWidgetFactory(object):
def __init__(self):
pass
def create_tile_widget(self, tile):
tile.widget = Image(source=tile.image_source,
size_hint=(None, None))
return tile.widget
def create_actor_widget(self, actor):
actor.widget = Image(source='Tmp_frame_black.png',
size_hint=(None, None))
return actor.widget

Okay, solved it myself. It turns out that if I supply the size to child widgets in factory, they are positioned properly. Although it solves the problem I have, I'd still be grateful if someone can explain where this quirk does come from.

Related

QGraphicsItem don't change pen of parent when chaning child

I have a multiple QGraphicsItems that are in a parent-child hierarchy. I'm trying to get highlighting of an item on mouse hover to work on an item basis, meaning that if the mouse hovers over an item it should highlight.
The highlighting works fine, but if I'm performing the highlighting on a child, then the highlighting is also automatically happening on the parent of such, which is not desired. Here is a code example of the problem
from PySide2.QtCore import Qt
from PySide2.QtGui import QPen
from PySide2.QtWidgets import QGraphicsItem, \
QGraphicsScene, QGraphicsView, QGraphicsLineItem, QGraphicsSceneHoverEvent, \
QGraphicsRectItem, QMainWindow, QApplication
class TextBox(QGraphicsRectItem):
def __init__(self, parent: QGraphicsItem, x: float, y: float, width: float, height: float):
super().__init__(parent)
self.setParentItem(parent)
self.setAcceptHoverEvents(True)
pen = QPen(
Qt.white,
2,
Qt.SolidLine,
Qt.RoundCap,
Qt.RoundJoin
)
self.setPen(pen)
self.setRect(x, y, width, height)
def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent):
super().hoverEnterEvent(event)
current_pen = self.pen()
current_pen.setWidth(5)
self.setPen(current_pen)
def hoverLeaveEvent(self, event: QGraphicsSceneHoverEvent):
super().hoverLeaveEvent(event)
current_pen = self.pen()
current_pen.setWidth(2)
self.setPen(current_pen)
class LineItem(QGraphicsLineItem):
def __init__(
self,
x1_pos: float,
x2_pos: float,
y1_pos: float,
y2_pos: float,
parent: QGraphicsItem = None
):
super().__init__()
self.setParentItem(parent)
self.setAcceptHoverEvents(True)
pen = QPen(
Qt.white,
2,
Qt.SolidLine,
Qt.RoundCap,
Qt.RoundJoin
)
self.setPen(pen)
self.setLine(
x1_pos,
y1_pos,
x2_pos,
y2_pos
)
def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent):
super().hoverEnterEvent(event)
current_pen = self.pen()
current_pen.setWidth(5)
self.setPen(current_pen)
def hoverLeaveEvent(self, event: QGraphicsSceneHoverEvent):
super().hoverLeaveEvent(event)
current_pen = self.pen()
current_pen.setWidth(2)
self.setPen(current_pen)
class DiagramScene(QGraphicsScene):
def __init__(self, parent=None):
super().__init__(parent)
self.setBackgroundBrush(Qt.black)
line_item = LineItem(0, 200, 0, 0)
box = TextBox(line_item, 0, 0, 20, 20)
self.addItem(line_item)
class GraphicsView(QGraphicsView):
def __init__(self):
self.scene = DiagramScene()
super().__init__(self.scene)
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.setCentralWidget(GraphicsView())
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
mainWindow = MainWindow()
mainWindow.setGeometry(100, 100, 800, 500)
mainWindow.show()
sys.exit(app.exec_())
When hovering over the line (which is the parent) then only the line gets highlighted. However, when hovering over the rectangle then both get highlighted. I assume it has something to do with that the rectangle is a child of the line item.
I would like to keep the parent-child hierarchy because I have to calculate child positions based on the parent positions which is easier that way.
Is there a way to not cascade the highlighting of the child item up to the parent as?
A possible solution is to check if the event position is actually within the item's boundingRect(), but this would only work for items that extend only vertically and horizontally. Since lines can also have a certain angle, it's better to check against the shape() instead:
def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent):
super().hoverEnterEvent(event)
if self.shape().contains(event.pos()):
current_pen = self.pen()
current_pen.setWidth(5)
self.setPen(current_pen)
The "leave" part is a bit trickier, though: the parent doesn't receive a leave event when hovering on a child, and it requires adding a scene event filter on all children.
Since a scene event filter can only be installed when an item is on a scene, you must try to install the filter both when a child is added or when the parent is added to a scene.
To simplify things, I created two similar pens with different widths, so that you only need to use setPen() instead of continuously getting the current one and change it.
class LineItem(QGraphicsLineItem):
def __init__(
self,
x1_pos: float,
x2_pos: float,
y1_pos: float,
y2_pos: float,
parent: QGraphicsItem = None
):
super().__init__()
self.setParentItem(parent)
self.setAcceptHoverEvents(True)
self.normalPen = QPen(
Qt.green,
2,
Qt.SolidLine,
Qt.RoundCap,
Qt.RoundJoin
)
self.hoverPen = QPen(self.normalPen)
self.hoverPen.setWidth(5)
self.setPen(self.normalPen)
self.setLine(
x1_pos,
y1_pos,
x2_pos,
y2_pos
)
def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent):
super().hoverEnterEvent(event)
if self.shape().contains(event.pos()):
self.setPen(self.hoverPen)
def hoverLeaveEvent(self, event: QGraphicsSceneHoverEvent):
super().hoverLeaveEvent(event)
self.setPen(self.normalPen)
def itemChange(self, change, value):
if change == self.ItemChildAddedChange and self.scene():
value.installSceneEventFilter(self)
elif change == self.ItemChildRemovedChange:
value.removeSceneEventFilter(self)
elif change == self.ItemSceneHasChanged and self.scene():
for child in self.childItems():
child.installSceneEventFilter(self)
return super().itemChange(change, value)
def sceneEventFilter(self, child, event):
if event.type() == event.GraphicsSceneHoverEnter:
self.setPen(self.normalPen)
elif (event.type() == event.GraphicsSceneHoverLeave and
self.shape().contains(child.mapToParent(event.pos()))):
self.setPen(self.hoverPen)
return super().sceneEventFilter(child, event)

How to update the size of the parent widget from the size of the children in Kivy?

This snippet that runs on Kivy for Python draws some rectangles (Boxes) as child widgets of a parent one (RootWidget).
By pressing ALT + D you create another box (added to the RootWidget).
I'm trying to implement a touch and drag behavior on the parent widget so that it moves all the child boxes together when they are dragged with the mouse.
However, the on_touch_down method (see self.collide_point(*touch.pos)) just gets the position of the original child widget (the one created by default) but not of the newly created ones.
Why? Is there a way to update the size of the parent so that it gets grabbed when a box other than the first is touched?
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.graphics import Ellipse, Color, Rectangle, Line
from kivy.core.window import Window
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.properties import NumericProperty, ListProperty
from random import randint
Builder.load_string('''
<Box>:
canvas:
Color:
rgba: .1, .1, 1, .9
Line:
width: 2.
rectangle: (self.x, self.y, self.width, self.height)
''')
class Tree(Widget):
pass
class Node(Widget):
pass
class Box(Widget):
def __init__(self, **kwargs):
super(Box, self).__init__(**kwargs)
self.size = [500, 300]
self.height = self.size[1]
self.width = self.size[0]
self.pos = [500,200]
# bind change of pos to redraw
self.bind(pos=self.redraw, size=self.redraw)
def redraw(self, *args):
# compute head and sisters' positions
self.x = self.pos[0]
self.y = self.pos[1]
#self.height = self.size[0]
#self.width = self.size[1]
class Branches(Widget):
pass
class Terbranch(Widget):
pass
class RootWidget(Widget):
def __init__(self, **kwargs):
super(RootWidget, self).__init__(**kwargs)
for i in range(2):
self.add_widget(Box())
self._keyboard = Window.request_keyboard(
self._keyboard_closed, self, 'text')
if self._keyboard.widget:
# If it exists, this widget is a VKeyboard object which you can use
# to change the keyboard layout.
pass
self._keyboard.bind(on_key_down=self._on_keyboard_down)
self.bind(pos=self.redraw, size=self.redraw)
def redraw (self, *args):
pass
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
# if the touch collides with our widget, let's grab it
touch.grab(self)
#print ('touched')
# and accept the touch.
return True
return super(RootWidget, self).on_touch_down(touch)
def on_touch_up(self, touch):
# check if it's a grabbed touch event
if touch.grab_current is self:
# don't forget to ungrab ourself, or you might have side effects
touch.ungrab(self)
# and accept the last up
return True
return super(RootWidget, self).on_touch_up(touch)
def on_touch_move(self, touch):
# check if it's a grabbed touch event
if touch.grab_current is self:
#self.pos = touch.pos
self.pos[0] += touch.dx
self.pos[1] += touch.dy
#redraw moved children
for child in self.children:
child.pos[0] += touch.dx
child.pos[1] += touch.dy
child.redraw()
# and accept the last move
return True
return super(RootWidget, self).on_touch_move(touch)
def _keyboard_closed(self):
print('My keyboard have been closed!')
self._keyboard.unbind(on_key_down=self._on_keyboard_down)
self._keyboard = None
def _on_keyboard_down(self, keyboard, keycode, text, modifiers):
#print('The key', keycode, 'have been pressed')
#print(' - text is %r' % text)
#print(' - modifiers are %r' % modifiers)
# Keycode is composed of an integer + a string
# If we hit escape, release the keyboard
if keycode[1] == 'escape':
keyboard.release()
elif keycode[1] == 'd' and modifiers[0] == 'alt':
newbox = Box()
self.add_widget(newbox)
# Return True to accept the key. Otherwise, it will be used by
# the system.
return True
def update(self, dt):
pass
class MyApp(App):
def build(self):
rw = RootWidget()
#Clock.schedule_interval(rw.update, 0.2)
return rw
if __name__ == "__main__":
MyApp().run()
Not sure I understand your question completely, but in your on_touch_move method you are moving all the child Box instances. But you are also changing the pos of the RootWidget itself. Since the RootWidget is the root window of the App, changing its pos property doesn't have any visual effect. However, that change affects the self.collide_point method (it uses pos and size to check for collision). So, if I understand your question, you just need to eliminate changing the pos of the RootWidget:
def on_touch_move(self, touch):
# check if it's a grabbed touch event
if touch.grab_current is self:
#self.pos = touch.pos
# comment out the next two lines
#self.pos[0] += touch.dx
#self.pos[1] += touch.dy
#redraw moved children
for child in self.children:
child.pos[0] += touch.dx
child.pos[1] += touch.dy
child.redraw()
# and accept the last move
return True

Why is 'root' keyword treated differently when it is called in Kivy?

How to make circular progress bar in kivy?
I have found a source above for circular progress bar and noted something weird.
from kivy.app import App
from kivy.uix.progressbar import ProgressBar
from kivy.core.text import Label as CoreLabel
from kivy.lang.builder import Builder
from kivy.graphics import Color, Ellipse, Rectangle
from kivy.clock import Clock
class CircularProgressBar(ProgressBar):
def __init__(self, **kwargs):
super(CircularProgressBar, self).__init__(**kwargs)
# Set constant for the bar thickness
self.thickness = 40
# Create a direct text representation
self.label = CoreLabel(text="0%", font_size=self.thickness)
# Initialise the texture_size variable
self.texture_size = None
# Refresh the text
self.refresh_text()
# Redraw on innit
self.draw()
def draw(self):
with self.canvas:
# Empty canvas instructions
self.canvas.clear()
# Draw no-progress circle
Color(0.26, 0.26, 0.26)
Ellipse(pos=self.pos, size=self.size)
# Draw progress circle, small hack if there is no progress (angle_end = 0 results in full progress)
Color(1, 0, 0)
Ellipse(pos=self.pos, size=self.size,
angle_end=(0.001 if self.value_normalized == 0 else self.value_normalized*360))
# Draw the inner circle (colour should be equal to the background)
Color(0, 0, 0)
Ellipse(pos=(self.pos[0] + self.thickness / 2, self.pos[1] + self.thickness / 2),
size=(self.size[0] - self.thickness, self.size[1] - self.thickness))
# Center and draw the progress text
Color(1, 1, 1, 1)
Rectangle(texture=self.label.texture, size=self.texture_size,
pos=(self.size[0]/2 - self.texture_size[0]/2, self.size[1]/2 - self.texture_size[1]/2))
def refresh_text(self):
# Render the label
self.label.refresh()
# Set the texture size each refresh
self.texture_size = list(self.label.texture.size)
def set_value(self, value):
# Update the progress bar value
self.value = value
# Update textual value and refresh the texture
self.label.text = str(int(self.value_normalized*100)) + "%"
self.refresh_text()
# Draw all the elements
self.draw()
class Main(App):
def just_function(self):
print(self.root) # <----- this will print None
# Simple animation to show the circular progress bar in action
def animate(self, dt):
print(self.root) # <---- this prints CircularProgressBar object
if self.root.value < 80:
self.root.set_value(self.root.value + 1)
else:
self.root.set_value(0)
# Simple layout for easy example
def build(self):
container = Builder.load_string(
'''CircularProgressBar:
size_hint: (None, None)
height: 200
width: 200
max: 80''')
# Animate the progress bar
Clock.schedule_interval(self.animate, 0.1)
print(self.root) # <---- this prints None
self.just_function() # <---- this prints None
return container
if __name__ == '__main__':
Main().run()
When you take a look at Main(App)
In this source, self.root is considered as CircularProgressBar in here.
But, when I do print(self.root) it prints None.
It only recognize CircularProgressBar when self.root is in a function that is called by Clock.scheduled_interval(func, rate).
Does anyone know what is happening in here?
The explanation is very simple and is clearly explained in the docs:
root = None
The root widget returned by the build() method or by the load_kv()
method if the kv file contains a root widget.
From the above it is understood that the root is the element that is returned in the build() method, so before something returns that function the root will be None, so when you print self.root within build() or call a function that prints self.root before returning that function you will always get None. After returning the root it will be what you returned, that is, the container an object of the class CircularProgressBar.

Adding additional parameters to QWidget subclass using PySide

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:

Refreshing QGraphicsScene pyqt - Odd results

I am trying to build a simple node graph in pyqt. I am having some trouble with a custom widget leaving artifacts and not drawing correctly when moved by the mouse. See images below:
Before I move the node with the mouse.
After I move the mode with the mouse
I thought that maybe it was the bounding method on my custom widget called nodeGFX:
def boundingRect(self):
"""Bounding."""
return QtCore.QRectF(self.pos().x(),
self.pos().y(),
self.width,
self.height)
Any Ideas guys? Complete py file below.
"""Node graph and related classes."""
from PyQt4 import QtGui
from PyQt4 import QtCore
# import canvas
'''
TODO
Function
- Delete Connection
- Delete nodes
Look
- Use look information from settings
- nodes
- connections
- canvas
'''
# ----------------------------- NodeGFX Class --------------------------------#
# Provides a visual repersentation of a node in the node interface. Requeres
# canvas interface. Added to main scene
#
class NodeGFX(QtGui.QGraphicsItem):
"""Display a node."""
# --------------------------- INIT ---------------------------------------#
#
# Initlize the node
# n_x - Where in the graphics scene to position the node. x cord
# n_y - Where in the graphics scene to position the node. y cord
# n_node - Node object from Canvas. Used in construction of node
# n_scene - What is the parent scene of this object
#
def __init__(self, n_x, n_y, n_node, n_scene):
"""INIT."""
super(NodeGFX, self).__init__()
# Colection of input and output AttributeGFX Objects
self.gscene = n_scene
self.inputs = {}
self.outputs = {}
# An identifier for selections
self.io = "node"
# The width of a node - TODO implement in settings!
self.width = 350
# Use information from the passed in node to build
# this object.
self.name = n_node.name
node_inputs = n_node.in_attributes
node_outputs = n_node.out_attributes
# How far down to go between each attribute TODO implement in settings!
attr_offset = 25
org_offset = attr_offset
if len(node_inputs) > len(node_outputs):
self.height = attr_offset * len(node_inputs) + (attr_offset * 2)
else:
self.height = attr_offset * len(node_outputs) + (attr_offset * 2)
# Create the node!
'''
QtGui.QGraphicsRectItem.__init__(self,
n_x,
n_y,
self.width,
self.height,
scene=n_scene)
'''
self.setFlag(QtGui.QGraphicsItem.ItemIsMovable, True)
self.lable = QtGui.QGraphicsTextItem(self.name,
self)
# Set up inputs
for key, value in node_inputs.iteritems():
new_attr_gfx = AttributeGFX(0,
0,
self.scene(),
self,
key,
value.type,
"input")
new_attr_gfx.setPos(0, attr_offset)
self.inputs[key] = new_attr_gfx
attr_offset = attr_offset + 25
# set up Outputs
attr_offset = org_offset
for key, value in node_outputs.iteritems():
new_attr_gfx = AttributeGFX(0,
0,
self.scene(),
self,
key,
value.type,
"output")
new_attr_gfx.setPos(self.width, attr_offset)
self.outputs[key] = new_attr_gfx
attr_offset = attr_offset + 25
# ---------------- Utility Functions -------------------------------------#
def canv(self):
"""Link to the canvas object."""
return self.scene().parent().parent().canvasobj
def __del__(self):
"""Destory a node and all child objects."""
# Remove self from GFX scene
print "Node del func called"
self.scene().removeItem(self)
def boundingRect(self):
"""Bounding."""
return QtCore.QRectF(self.pos().x(),
self.pos().y(),
self.width,
self.height)
def mousePressEvent(self, event):
self.update()
super(NodeGFX, self).mousePressEvent(event)
def mouseReleaseEvent(self, event):
self.update()
super(NodeGFX, self).mouseReleaseEvent(event)
# ------------- Event Functions ------------------------------------------#
def mouseMoveEvent(self, event):
"""Update connections when nodes are moved."""
self.scene().updateconnections()
QtGui.QGraphicsItem.mouseMoveEvent(self, event)
self.gscene.update()
def mousePressEvent(self, event):
"""Select a node."""
print "Node Selected"
self.scene().selection(self)
QtGui.QGraphicsEllipseItem.mousePressEvent(self, event)
# ----------- Paint Functions -------------------------------------------#
def paint(self, painter, option, widget):
painter.setPen(QtCore.Qt.NoPen)
painter.setBrush(QtCore.Qt.darkGray)
self.width = 400
self.height = 400
painter.drawEllipse(-7, -7, 20, 20)
rectangle = QtCore.QRectF(0,
0,
self.width,
self.height)
painter.drawRoundedRect(rectangle, 15.0, 15.0)
# ----------------------------- NodeGFX Class --------------------------------#
# Provides a visual repersentation of a Connection in the node interface.
# Requeres canvas interface and two nodes. Added to main scene
# Using two attributes draw a line between them. When
# Set up, a connection is also made on the canvas. unlike the canvas which
# stores connections on attributes, connectionGFX objects are stored in a
# list on the scene object
#
class ConnectionGFX (QtGui.QGraphicsLineItem):
"""A connection between two nodes."""
# ---------------------- Init Function -----------------------------------#
#
# Inits the Connection.
# n_scene - The scene to add these connections to
# n_upsteam - a ref to an upstream attributeGFX object.
# n_downstream - a ref to a downstream attributeGFX object.
#
def __init__(self, n_scene, n_upstream, n_downstream):
"""INIT."""
# Links to the AttributeGFX objs
self.upstreamconnect = n_upstream
self.downstreamconnect = n_downstream
self.io = 'connection'
super(ConnectionGFX, self).__init__(scene=n_scene)
self.setFlag(QtGui.QGraphicsItem.ItemIsSelectable, True)
self.scene().addItem(self)
self.update()
# ----------------- Utility functions -------------------------------
# When nodes are moved update is called. This will change the line
def update(self):
"""Called when new Draw."""
super(ConnectionGFX, self).update()
x1, y1, x2, y2 = self.updatepos()
self.setLine(x1, y1, x2, y2)
# Called by update calculate the new line points
def updatepos(self):
"""Get new position Data to draw line."""
up_pos = QtGui.QGraphicsItem.scenePos(self.upstreamconnect)
dn_pos = QtGui.QGraphicsItem.scenePos(self.downstreamconnect)
x1 = up_pos.x()
y1 = up_pos.y()
x2 = dn_pos.x()
y2 = dn_pos.y()
return x1, y1, x2, y2
# -------------------------- Event Overides ------------------------------#
def mousePressEvent(self, event):
"""Select a connection."""
print "Connection Selected"
self.scene().selection(self)
QtGui.QGraphicsEllipseItem.mousePressEvent(self, event)
# ------------------------ AttributeGFX Class --------------------------------#
# Provides a visual repersentation of an attribute. Used for both input and
# output connections. Stored on nodes themselves. They do not hold any of
# the attribute values. This info is stored and modded in the canvas.
#
class AttributeGFX (QtGui.QGraphicsEllipseItem):
"""An attribute on a node."""
# ---------------- Init -------------------------------------------------#
#
# Init the attributeGFX obj. This object is created by the nodeGFX obj
# n_x - Position x
# n_y - Position y
# n_scene - The scene to add this object to
# n_parent - The patent node of this attribute. Used to link
# n_name - The name of the attribute, must match whats in canvas
# n_type - The data type of the attribute
# n_io - Identifier for selection
def __init__(self,
n_x,
n_y,
n_scene,
n_parent,
n_name,
n_type,
n_io):
"""INIT."""
self.width = 15
self.height = 15
self.io = n_io
self.name = n_name
# Use same object for inputs and outputs
self.is_input = True
if "output" in n_io:
self.is_input = False
QtGui.QGraphicsEllipseItem.__init__(self,
n_x,
n_y,
self.width,
self.height,
n_parent,
n_scene)
self.lable = QtGui.QGraphicsTextItem(n_name, self, n_scene)
# self.lable.setY(n_y)
# TODO - Need a more procedual way to place the outputs...
if self.is_input is False:
n_x = n_x - 100
# self.lable.setX(self.width + n_x)
self.lable.setPos(self.width + n_x, n_y)
# ----------------------------- Event Overides -------------------------- #
def mousePressEvent(self, event):
"""Select and attribute."""
print "Attr Selected"
self.scene().selection(self)
QtGui.QGraphicsEllipseItem.mousePressEvent(self, event)
# ------------------------ SceneGFX Class --------------------------------#
# Provides tracking of all the elements in the scene and provides all the
# functionality. Is a child of the NodeGraph object. Commands for editing the
# node network byond how they look in the node graph are passed up to the
# canvas. If the functions in the canvas return true then the operation is
# permitted and the data in the canvas has been changed.
#
class SceneGFX(QtGui.QGraphicsScene):
"""Stores grapahic elems."""
# -------------------------- init -------------------------------------- #
#
# n_x - position withing the node graph widget x cord
# n_y - position withing the node graph widget y cord
def __init__(self, n_x, n_y, n_width, n_height, n_parent):
"""INIT."""
# Dict of nodes. Must match canvas
self.nodes = {}
# list of connections between nodes
self.connections = []
# The currently selected object
self.cur_sel = None
# how far to off set newly created nodes. Prevents nodes from
# being created ontop of each other
self.node_creation_offset = 100
super(SceneGFX, self).__init__(n_parent)
self.width = n_width
self.height = n_height
def addconnection(self, n1_node, n1_attr, n2_node, n2_attr):
"""Add a new connection."""
new_connection = ConnectionGFX(self,
self.nodes[n1_node].outputs[n1_attr],
self.nodes[n2_node].inputs[n2_attr])
self.connections.append(new_connection)
self.parent().update_attr_panel()
def helloworld(self):
"""test."""
print "Scene - hello world"
def updateconnections(self):
"""Update connections."""
for con in self.connections:
con.update()
def canv(self):
"""Link to the canvas object."""
return self.parent().canvasobj
def mainwidget(self):
"""Link to the main widget obj."""
return self.parent()
def delselection(self):
"""Delete the selected obj."""
if "connection" in self.cur_sel.io:
print "Deleteing Connection"
if self.mainwidget().delete_connection(self.cur_sel):
self.removeItem(self.cur_sel)
for x in range(0, len(self.connections) - 1):
if self.cur_sel == self.connections[x]:
del self.connections[x]
break
self.cur_sel = None
elif "node" in self.cur_sel.io:
if self.mainwidget().delete_node(self.cur_sel):
print "Deleteing Node"
node_name = self.cur_sel.name
# First search for all connections assosiated with this node
# and delete
# Create Dic from list
connection_dict = {}
for x in range(0, len(self.connections)):
connection_dict[str(x)] = self.connections[x]
new_connection_list = []
for key, con in connection_dict.iteritems():
up_node = con.upstreamconnect.parentItem().name
down_node = con.downstreamconnect.parentItem().name
if up_node == node_name or down_node == node_name:
self.removeItem(connection_dict[key])
else:
new_connection_list.append(con)
self.connections = new_connection_list
del connection_dict
self.removeItem(self.nodes[node_name])
del self.nodes[node_name]
self.parent().update_attr_panel()
def keyPressEvent(self, event):
"""Listen for key presses on scene obj."""
if event.key() == QtCore.Qt.Key_Delete:
self.delselection()
super(SceneGFX, self).keyPressEvent(event)
def selection(self, sel):
"""Function to handel selections and connections."""
last_sel = self.cur_sel
self.cur_sel = sel
print "Last Sel:", last_sel
print "Current Sel:", self.cur_sel
if "node" in sel.io:
self.mainwidget().selected_node = sel
self.mainwidget().attr_panel.update_layout()
# Need to compaire the current and last selections to see
# if a connection has been made
if last_sel != None:
if "input" in last_sel.io and "output" in self.cur_sel.io:
lspn = last_sel.parentItem().name
cspn = self.cur_sel.parentItem().name
if lspn is not cspn:
print "Connecting Attrs 1"
self.mainwidget().connect(last_sel.parentItem().name,
last_sel.name,
self.cur_sel.parentItem().name,
self.cur_sel.name)
last_sel = None
self.cur_sel = None
elif "output" in last_sel.io and "input" in self.cur_sel.io:
lspn = last_sel.parentItem().name
cspn = self.cur_sel.parentItem().name
if lspn is not cspn:
print "Connecting Attrs 2"
self.mainwidget().connect(last_sel.parentItem().name,
last_sel.name,
self.cur_sel.parentItem().name,
self.cur_sel.name)
last_sel = None
self.cur_sel = None
class NodeGraph (QtGui.QGraphicsView):
"""Main Wrapper for node network."""
def __init__(self, p):
"""INIT."""
QtGui.QGraphicsView.__init__(self, p)
self.mainwin = p
self.initui()
self.nodes = {}
def initui(self):
"""Set up the UI."""
self.setFixedSize(1000, 720)
self.scene = SceneGFX(0, 0, 25, 1000, self.mainwin)
self.setScene(self.scene)
def addnode(self, node_name, node_type):
"""Forward node creation calls to scene."""
br = self.mapToScene(self.viewport().geometry()).boundingRect()
x = br.x() + (br.width()/2)
y = br.y() + (br.height()/2)
new_node = NodeGFX(x,
y,
self.canv().nodes[node_name],
self)
self.scene.addItem(new_node)
self.nodes[node_name] = new_node
def addconnection(self, n1_node, n1_attr, n2_node, n2_attr):
"""Add a connection between 2 nodes."""
self.scene.addconnection(n1_node, n1_attr, n2_node, n2_attr)
def helloworld(self):
"""test."""
print "Node graph - hello world"
def canv(self):
"""Link to the canvas object."""
return self.mainwin.canvasobj
def change_name_accepted(self, old_name, new_name):
"""Update the node graph to accept new names"""
pass
So my issue was that I was not scaling the bounding box in "object space"... To following changes fix my issue.
def boundingRect(self):
"""Bounding."""
# Added .5 for padding
return QtCore.QRectF(-.5,
-.5,
self.width + .5,
self.height + .5)
def paint(self, painter, option, widget):
painter.setPen(QtCore.Qt.NoPen)
painter.setBrush(QtCore.Qt.darkGray)
self.width = 400
self.height = 400
rectangle = QtCore.QRectF(0,
0,
self.width,
self.height)
painter.drawRoundedRect(rectangle, 15.0, 15.0)

Categories