带有PyQt4的十六进制网格图

时间:2013-08-27 11:14:47

标签: python qt pyqt4

我正在尝试创建一个地图编辑器。我打算将地图设为六边形网格,其中每个六边形都是地图的图块。瓷砖将是该区域(海洋,草地,沙漠,山脉等)的图形表示。地图应具有任何尺寸。我们暂时冻结这里的要求:)

我想使用PyQt4(将其作为设计要求)。由于我刚刚开始使用Qt / PyQt,我面临着巨大的问题:这个Qt这么大,我无法理解它。在这里,我要求你的亲切和最受欢迎的经历。

经过一番谷歌搜索后,我决定使用QGraphicalView / Scene方法。实际上,我正在考虑创建自己的hexgrid类,继承自QGraphicalView并创建继承自QGraphicalPolygonItem的RegularPolygon类。

现在他们遇到了疑惑和问题。

我的主要怀疑是“我的方法是正确的吗?”想想我在帖子开头解释的需求:六边形地图,每个六边形将是特定类型的瓷砖(海洋,沙漠,草地,山脉等)。一旦编辑器工作,我就会关注性能(滚动会感觉很好吗?还有这种事情)。

到目前为止,问题在于精确度。我通过创建和绘制所有六边形来绘制hexgrid(这对我来说甚至听起来很糟糕......考虑性能)。我使用了一些公式来计算每个六边形的顶点并从那里创建多边形。我希望两个连续六边形的边完全在同一个位置重合,但是四舍五入似乎与我的欲望有点相似,因为有时六边形边在同一个位置完美匹配(好)有时它们不匹配什么似乎是1像素差异(坏)。这给网格带来了糟糕的视觉印象。也许我没有很好地解释自己...如果我给你代码你自己运行它会更好

总结:

  1. 您认为我的方法会给出未来的性能问题吗?
  2. 为什么六边形不能完全放置以便它们共用边?如何避免这个问题?
  3. 代码:

    #!/usr/bin/python
    """
    Editor of the map.
    """
    
    __meta__ =  \
    {
        (0,0,1): (
                  [ "Creation" ],
                  [ ("Victor Garcia","vichor@xxxxxxx.xxx") ]
                 )
    } 
    
    import sys, math
    from PyQt4 import QtCore, QtGui
    
    # ==============================================================================
    class HexGrid(QtGui.QGraphicsView):
        """
        Graphics view for an hex grid.
        """
    
        # --------------------------------------------------------------------------
        def __init__(self, rect=None, parent=None):
            """
            Initializes an hex grid. This object will be a GraphicsView and it will
            also handle its corresponding GraphicsScene.
                rect -- rectangle for the graphics scene.
                parent -- parent widget
            """
            super(HexGrid,self).__init__(parent)
    
            self.scene = QtGui.QGraphicsScene(self)
            if rect != None: 
                if isinstance(rect, QtCore.QRectF): self.scene.setSceneRect(rect)
                else: raise StandardError ('Parameter rect should be QtCore.QRectF')
            self.setScene(self.scene)
    
    # ==============================================================================
    class QRegularPolygon(QtGui.QGraphicsPolygonItem):
        """
        Regular polygon of N sides
        """
    
        def __init__(self, sides, radius, center, angle = None, parent=None):
            """
            Initializes an hexagon of the given radius.
                sides -- sides of the regular polygon
                radius -- radius of the external circle
                center -- QPointF containing the center
                angle -- offset angle in radians for the vertices
            """
            super(QRegularPolygon,self).__init__(parent)
    
            if sides < 3: 
                raise StandardError ('A regular polygon at least has 3 sides.')
            self._sides = sides
            self._radius = radius
            if angle != None: self._angle = angle
            else: self._angle = 0.0
            self._center = center
    
            points = list()
            for s in range(self._sides):
                angle = self._angle + (2*math.pi * s/self._sides)
                x = center.x() + (radius * math.cos(angle))
                y = center.y() + (radius * math.sin(angle))
                points.append(QtCore.QPointF(x,y))
    
            self.setPolygon( QtGui.QPolygonF(points) )
    
    
    # ==============================================================================
    def main():
        """
        That's it: the  main function
        """
        app = QtGui.QApplication(sys.argv)
    
        grid = HexGrid(QtCore.QRectF(0.0, 0.0, 500.0, 500.0))
    
        radius = 50
        sides = 6
    
        apothem = radius * math.cos(math.pi/sides)
        side = 2 * apothem * math.tan(math.pi/sides)
    
        xinit = 50
        yinit = 50
        angle = math.pi/2
        polygons = list()
    
        for x in range(xinit,xinit+20):
            timesx = x - xinit
            xcenter = x + (2*apothem)*timesx
            for y in range(yinit, yinit+20):
                timesy = y - yinit
                ycenter = y + ((2*radius)+side)*timesy
    
                center1 = QtCore.QPointF(xcenter,ycenter)
                center2 = QtCore.QPointF(xcenter+apothem,ycenter+radius+(side/2))
    
                h1 = QRegularPolygon(sides, radius, center1, angle)
                h2 = QRegularPolygon(sides, radius, center2, angle)
    
                # adding polygons to a list to avoid losing them when outside the
                # scope (loop?). Anyway, just in case
                polygons.append(h1)
                polygons.append(h2)
    
                grid.scene.addItem(h1)
                grid.scene.addItem(h2)
    
        grid.show()
        app.exec_()
    
    # ==============================================================================
    if __name__ == '__main__':
        main()
    

    最后但并非最不重要的是,对于长篇文章感到抱歉:)

    由于 维克多

4 个答案:

答案 0 :(得分:3)

就个人而言,我将每个六边形图块定义为单独的SVG图像,并且每当缩放级别改变时,使用QImage和QSvgRenderer类将它们渲染到QPixmaps(带有alpha通道)。我将创建一个QGraphicsItem子类来显示每个tile。

诀窍是选择缩放级别,使(直立)六边形的宽度为2的倍数,高度为4的倍数,宽度/高度约为sqrt(3/4)。六边形在任一方向上略微压扁,但对于直径至少为8像素的所有六边形,效果都不易察觉。

如果六边形的宽度为2*w,高度为4*h,则以下是将(直立)六边形映射到笛卡尔坐标的方法:

Hexagon in a rectangular grid

如果六边形的每一边都是a,那么h=a/2w=a*sqrt(3)/2w/h=sqrt(3)

为获得最佳显示质量,请选择整数wh,使其比率约为sqrt(3) ≃ 1.732。这意味着你的六边形会被轻微压扁,但这没关系;它是不可察觉的。

因为坐标现在总是整数,所以你可以安全地(没有显示人工制品)使用预渲染的六边形瓷砖,只要它们有一个alpha通道,并且可能是边框以允许更平滑的alpha过渡。每个矩形图块的宽度为2*w+2*b像素,4*h+2*b像素高,其中b是额外边框(重叠)像素的数量。

需要额外的边框以避免可察觉的接缝(背景颜色渗透),其中像素在所有重叠的瓷砖中仅部分不透明。边框允许您更好地将瓷砖混合到相邻的瓷砖中;如果在SVG图块中包含一个小的边框区域,SVG渲染器将自动执行某些操作。

如果您使用x向右增长且y向下的典型屏幕坐标,则六角形X,Y相对于0,0的坐标是微不足道的:

y = 3*h*Y
if Y is even, then:
    x = 2*w*X
else:
    x = 2*w*X + w

显然,奇数行的六边形位于右半边六边形。

对QGraphicsItem进行子类化并使用边界多边形(用于鼠标和交互测试)意味着当您想知道鼠标悬停在哪个六角形图块上时,Qt将为您完成所有繁重的工作。

但是,你可以进行逆映射 - 从屏幕坐标回到六边形 - 你自己。

首先,计算坐标对所在的矩形网格单元格(上图中的绿色网格线):

u = int(x / w)
v = int(y / h)

假设所有坐标都是非负的。否则,%必须被理解为“非负余数,除以”。 (即0 <= a % b < b代表所有a,甚至是负a; b此处始终为正整数。)

如果原点如上图所示,那么每三个中的两行是微不足道的,除了每个奇数行的六边形向右移动一个网格单元格:

if v % 3 >= 1:
    if v % 6 >= 4:
        X = int((u - 1) / 2)
        Y = int(v / 3)
    else:
        X = int(u / 2)
        Y = int(v / 3)

每隔三行包含一个带有对角线边界的矩形网格单元格,但不要担心:如果边界是\(wrt。高于图像),您只需要检查是否

    (x % w) * h   >=   (y % h) * w

了解你是否在右上角的三角形部分。如果边界是/ wrt。在上图中,您只需要检查是否

    (x % w) * h + (y % h) * w   >=   (w * h - (w + h) / 2)

了解你是否在右下角的三角形部分。

在矩形网格单元的每个四列和六行部分中,有八种情况需要使用上述测试条款之一进行处理。 (我懒得在这里为你准确的if条款;就像我说的那样,我让Qt为我这样做。)这个矩形区域正好重复整个六边形地图;因此,完整的坐标转换可能需要最多9个if子句(取决于你如何写它),所以写起来有点烦人。

如果您想确定例如相对于它悬停的六边形的鼠标光标位置,首先使用上面的方法确定鼠标悬停在哪个六边形上,然后从鼠标坐标中减去该六边形的坐标,得到相对于当前六边形的坐标。

答案 1 :(得分:1)

尝试使用此main()函数。我使用了内切圆(ri)的半径而不是你使用的外接圆(半径)。它现在看起来好一点,但仍然不完美。我认为在六边形的顶部和底部绘制斜边的方式是不同的。

def main():
    """
    That's it: the  main function
    """
    app = QtGui.QApplication(sys.argv)

    grid = HexGrid(QtCore.QRectF(0.0, 0.0, 500.0, 500.0))

    radius = 50 # circumscribed circle radius
    ri = int(radius / 2 * math.sqrt(3)) # inscribed circle radius
    sides = 6

    apothem = int(ri * math.cos(math.pi/sides))
    side = int(2 * apothem * math.tan(math.pi/sides))

    xinit = 50
    yinit = 50
    angle = math.pi/2
    polygons = list()

    for x in range(xinit,xinit+20):
        timesx = x - xinit
        xcenter = x + (2*apothem-1)*timesx
        for y in range(yinit, yinit+20):
            timesy = y - yinit
            ycenter = y + ((2*ri)+side)*timesy

            center1 = QtCore.QPointF(xcenter,ycenter)
            center2 = QtCore.QPointF(xcenter+apothem,ycenter+ri+(side/2))

            h1 = QRegularPolygon(sides, ri, center1, angle)
            h2 = QRegularPolygon(sides, ri, center2, angle)

            # adding polygons to a list to avoid losing them when outside the
            # scope (loop?). Anyway, just in case
            polygons.append(h1)
            polygons.append(h2)

            grid.scene.addItem(h1)
            grid.scene.addItem(h2)

    grid.show()
    app.exec_()

答案 2 :(得分:1)

这里有很多问题。它们与Qt或Python没有特别关系,而是与通用计算机科学有关。

您希望在光栅设备上显示浮点几何形状,因此必须以某种方式进行浮点到整数转换。它不在您的代码中,因此它将发生在较低级别:图形库,显示驱动程序或其他任何内容。由于您对结果不满意,您必须自己处理此转换。

没有正确或错误的方法来做到这一点。例如,假设半径为“半径”为50的六角形图块。六边形的方向使得W顶点位于(-50,0),E顶点位于(50,0)。现在这个六边形的NE顶点大约是(25,0,43.3)。在N方向上与该相邻的六边形的中心在y = 86.6左右,顶边在129.9。你想怎么像这样?如果你将43.3向下舍入到43,那么现在你不再拥有数学上精确的正六边形。如果您将129.9向上舍入到130,则您的第一个六边形总高度为86像素,但顶部的六边形为87.这是您必须根据项目要求解决的问题。

这只是一种情况(半径= 50)。如果允许半径变化,你能想出一个算法来处理所有情况吗?我不能。我认为您需要为六边形使用固定的屏幕尺寸,或者至少将可能性减少到一小部分。

您的代码中没有任何地方确定显示窗口的大小,因此我不明白您打算如何处理缩放问题,或者确定显示完整地图需要多少个十六进制。

关于你的第一个问题,我确信表现会很差。 QRegularPolygon的构造函数位于创建六边形的循环内部,因此它被多次调用(在您的示例中为800)。它为每个顶点执行两次触发计算,因此在构建十六进制列表时执行9600触发计算。你不需要任何一个。计算是0度,60度,120度等的正弦和余弦。那些很容易你甚至不需要罪和cos。

使用trig函数也会加剧浮点/整数问题。看看这个:

>> int(50.0*math.sin(math.pi/6)) 
24

我们知道它应该是25,但计算机将其视为int(24.999999999996) - 我可能已经遗漏了几个9。

如果计算一个六边形的顶点位置,您可以通过简单的平移获得所有其他六边形。请参阅有用的Qt函数QPolygon-&gt; translate或QPolygon-&gt;翻译。

当您的设计概念绝对需要六边形时,您似乎不需要可以处理任何类型多边形的构造函数。你刚从某处复制过吗?我认为这主要是混乱,总是打开错误的大门。

答案 3 :(得分:0)

你真的需要多边形吗?后来,我想,游戏将使用光栅图像,因此多边形仅用于显示目的。 您可以采用表示多边形所有角落的点云,并在其下方绘制线条。有了这个,你可以避免圆角/浮点算术等问题。