hedgehog/software/ HedgehogPyScribble001
The Qt Scribble demo app rewritten in Python, which receives OSC messages that change the pen size,
both absolute via /penwidth
and relative via /penwidthby
, both of which take integer parameters.
Naturally other parameters such as colour could be controlled in the same way, and other events, as illustrated by /clear
.
That is the general idea, and all the clever workflow design can be factored out of the app – the core of the app
needs only to do as it is told. Part of the idea of Hedgehog is that the core of an app is controlled by receiving messages,
both from the App's own UI, and also from external sources, so that a user can control an app with anything he/she wishes.
Thus Our Hedgehogs can talk to our applications.
The point to make is how easy it can be to Hedgehog-enable an application. (Here OSC is used as the messaging protocol, and it probably will suffice, Hedgehog is the concept of controlling our apps with possibly-custom-designed peripherals with a varied array of knobs and buttons.) In this instance we are using PySide6, and Qt nicely provides us with a UdpSocket class that neatly integrates with Qt's signals and slots and its runloop. Basically an app needs to start a thread which listens on the socket and, when they are received, store them somewhere and notify the main runloop that there is a message to be processed. Once that is done, you can control your apps happily using whatever control surface you wish to fashion, be it made of Midi controllers, or something cooked up with an Arduino, or some commercial knobs-and-buttons peripherals like the TourBox.
scribble.py
from PySide6.QtCore import *
from PySide6.QtGui import *
from PySide6.QtWidgets import *
from PySide6.QtNetwork import *
import sys
from pythonosc.osc_message import OscMessage
app = QApplication(sys.argv)
class HHReceiver(QObject):
# this only works with datagrams containing single osc messages
def handleUdp(self):
while self.udpSocket.hasPendingDatagrams():
datagram = self.udpSocket.receiveDatagram(4096)
data = datagram.data().data()
print(f"Recv ({len(data)}): {data}")
try:
message = OscMessage(data)
except Exception as e:
print(f"#Fail to parse datagram ({e}) : {data}")
self.processMessage(message)
def processMessage(self,message):
addr = message.address
args = message.params
if addr == "/penwidth":
try:
newWidth = int(args[0])
self.scribble.setPenWidth(newWidth)
except IndexError:
print(f"/penwidth message must have single integer param")
elif addr == "/penwidthby":
try:
dx = int(args[0])
self.scribble.adjustPenWidthBy(dx)
except IndexError:
print(f"/penwidthby message must have single integer param")
elif addr == "/clear":
self.scribble.clearImage()
def __init__(self,scribble,port=2800):
self.scribble = scribble
self.udpSocket = QUdpSocket()
self.udpSocket.readyRead.connect(self.handleUdp)
self.udpSocket.bind(QHostAddress.Any,port)
class ScribbleArea(QWidget):
def __init__(self,*xs,**kw):
super().__init__(*xs,**kw)
self.setAttribute(Qt.WA_StaticContents)
self._modified = False
self._scribbling = False
self._penWidth = 1
self._penColor = Qt.blue
self.image = QImage()
self.lastPoint = QPoint()
def adjustPenWidthBy(self,dx):
self.setPenWidth(self._penWidth+dx)
def setPenColor(self, newColor : QColor):
self._penColor = newColor
def setPenWidth(self, newWidth : int):
self._penWidth = min(max(1,newWidth),100)
def isModified(self) -> bool:
return self._modified
def penColor(self) -> QColor:
return self._penColor
def penwidth(self) -> int:
return self._penWidth
def clearImage(self):
self.image.fill(qRgb(255,255,255))
self.modified = True
self.update()
def mousePressEvent(self,event):
if event.button() == Qt.LeftButton:
self.lastPoint = event.position().toPoint()
self.scribbling = True
def mouseMoveEvent(self,event):
if (event.buttons() & Qt.LeftButton) and self.scribbling:
self.drawLineTo(event.position().toPoint())
def mouseReleaseEvent(self,event):
if event.button() == Qt.LeftButton and self.scribbling:
self.drawLineTo(event.position().toPoint())
self.scribbling = False
def paintEvent(self,event):
with QPainter(self) as p:
dirtyRect = event.rect()
p.drawImage(dirtyRect,self.image,dirtyRect)
def resizeEvent(self,event):
w = self.width()
h = self.height()
iw = self.image.width()
ih = self.image.height()
if w > iw or h > ih:
nw = max(w+128,iw)
nh = max(h+128,ih)
self.resizeImage(self.image,QSize(nw,nh))
super().resizeEvent(event)
def drawLineTo(self,endPoint : QPoint):
with QPainter(self.image) as p:
p.setPen(QPen(self._penColor,self._penWidth,Qt.SolidLine,Qt.RoundCap,Qt.RoundJoin))
p.drawLine(self.lastPoint,endPoint)
self.modified = True
rad = int((self._penWidth // 3)*2)
self.update(QRect(self.lastPoint, endPoint).normalized().adjusted(-rad,-rad,+rad,+rad))
self.lastPoint = endPoint
def resizeImage(self,image : QImage, newSize: QSize):
if image.size() == newSize:
return
newImage = QImage(newSize, QImage.Format_RGB32)
newImage.fill(qRgb(255,255,255))
with QPainter(newImage) as p:
p.drawImage(QPoint(0,0),self.image)
self.image = newImage
scr = ScribbleArea()
scr.resize(600,500)
scr.show()
h = HHReceiver(scr)
exit(app.exec())
a trivial sender for interactive use
description = '''
load in interactive mode (i.e. python -i trivial_send.py)
then use s("/osc/message",[1,2,3,4]) to send
'''
import socket
from pythonosc import udp_client
client = udp_client.SimpleUDPClient("localhost",2800,family=socket.AF_INET)
def s(path,*args):
print(f"Sending {path=} {args=}")
client.send_message(path,args)
usage
python -i sender.py
example transcript of interactive session
>>> s("/penwidth",15)
Sending path='/penwidth' args=(15,)
>>> s("/penwidthby",15)
Sending path='/penwidthby' args=(15,)
>>> s("/clear")
Sending path='/clear' args=()