I'd like to hear your thoughts on Qt at some point too Triplefox if you're up for it.
I am always up for writing walls of text
Qt is a strong library overall. Pretty much every widget you can think of is built in; the only customized drawing/painting I've done for this project is based on QGraphicsScene/QGraphicsView, which provide an abstract scenegraph. Wiring up events has been very little trouble with the "slots and signals" event model, I just had a small learning curve to figure out the conventions and how they translated to PyQt vs. C++ Qt. Layout is easy to code up using the box or grid models. I tried using the designer tool and failed utterly which was kind of expected. It seems like all the GUI designer tools I've used are useful only if you're making forms of fixed size and static layout.
The place where it's been ugliest is in matching the data model with the views, because Qt has its own data structures, and then Python has different data structures, and since the widgets expect the Qt model I tend to end up making "blessed" ways of changing any given data structure that synchronize all representations. And I don't think that problem could really be "fixed," either - figuring out the data model seems to be most of making a GUI app.
Here is an example of the kind of thinking I went through yesterday and today to figure out stamp placement:
To display all the stamps on the map I have to add their QGraphicsPixmapItem to a QGraphicsScene. Mostly straightforward: I load a file into a Pixmap and instance the Item with it, position it, rotate it, et cetera. The problem is that each Item has a zValue parameter that controls depth; the QGraphicsScene is unordered. But what I really want to represent the stamps with is an ordered list, as that improves consistency between the tool and the game: if everything has the same zValue, the view will display things in an undefined order, which is no good for background art. Maintaining a list avoids this problem.
So I have two models now, the QGraphicsScene internal model and the list, and I have to figure out the best means of synchronizing both. The method I'm settling on is to instance a Stamp class that contains the QGraphicsPixmapItem; the Item contains a backreference to the Stamp. The Stamp doesn't know what it belongs to, but it doesn't need to - the Item does, because the Item is what will be returned from mouse methods that look at the Scene. I also set a QGraphicsItemGroup on the Item to make it belong to a layer. The QGraphicsItemGroup bundles a bunch of Items in the same way as a QGraphicsScene and lets me apply parent Z values to the items so that layers stack. I give it a backreference to a StampLayerModel which contains the list I originally wanted. Thus I get a way to access and modify everything from everywhere by using the methods of the Stamp, the QGraphicsScene, and the StampLayerModel depending on the situation.
Implementing this sort of thing is the "ugliness" that I keep encountering in my code, basically:
class Stamp:
"""A single stamp placed onto a map."""
def __init__(self,image="",x=0,y=0,rotation=0,xscale=1,yscale=1):
self.image = image
self.item = QtGui.QGraphicsPixmapItem(a.allimages[self.image],None, None)
self.item.stamp = self
rect = self.item.boundingRect()
self.item.setOffset(-rect.width()/2,-rect.height()/2)
self.setPos(x,y)
self.setRotation(rotation)
self.setScale(xscale,yscale)
def setPos(self,x,y):
self.x = x
self.y = y
self.item.setPos(self.x,self.y)
def setRotation(self,rotation):
self.rotation = rotation
self.item.rotate(rotation)
def setScale(self,xscale, yscale):
self.xscale = xscale
self.yscale = yscale
self.item.scale(xscale,yscale)
def json(self, stampdict):
return [stampdict[self.image],self.x,self.y,self.rotation,self.xscale,self.yscale]
class StampLayerModel:
"""A layer of stamps."""
def __init__(self, name="default"):
self.d = []
self.a = QtGui.QGraphicsItemGroup()
self.a.model = self
self.name = name
def add_stamp(self, stamp):
self.d.append(stamp)
stamp.item.setGroup(self.a)
stamp.item.setZValue(self.d.index(stamp))
def remove_stamp(self, oldstampitem):
idx = self.d.index(oldstampitem.stamp)
oldstampitem.stamp.item = None
oldstampitem.stamp = None
del(self.d[idx])
for n in range(len(self.d)):
self.d[n].item.setZValue(n)
def get_stamp(self, stampitem):
return stampitem.stamp
def replace_stamp(self, oldstampitem, newstamp):
idx = self.d.index(oldstampitem.stamp)
oldstampitem.setGroup(0)
oldstampitem.stamp.item = None
oldstampitem.stamp = None
self.d[idx] = newstamp
newstamp.item.setZValue(self.d.index(newstamp))
I haven't tested all the methods shown yet, but I can add stamps and position them OK. The "d" and "a" variable names are conventions used elsewhere to represent "data model" vs. "application settings+state". In most cases I'm dealing with static, app-wide variables, so I can correspondingly push them out to global containers with things like "a.map.brushnum" to get the selected brush of the map editor, but this case I have to maintain that distinction on a per-instance basis.