Skip to main content
 首页 » 编程设计

python之使用PyQt4的十六进制网格图

2025年05月04日18zhoujg

我正在尝试创建地图编辑器。我希望地图是一个六边形网格,其中每个六边形都是地图的图块。图块将是该区域(海洋,草地,沙漠,山脉等)的图形表示。该地图的大小不限。让我们暂时冻结需求:)

我想使用PyQt4(将其作为设计要求)。当我刚开始使用Qt / PyQt时,我面临着一个巨大的问题:这个Qt太大了,我无法全部掌握。我在这里,要求您的亲切和最欢迎的经历。

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

现在他们来了疑问和问题。

我的主要疑问是“我的方法正确吗?”考虑一下我在文章开头所解释的需求:六边形地图,其中每个六边形将是给定类型的瓷砖(海洋,沙漠,草地,山脉等)。编辑器工作后,我会担心性能(滚动会感觉不错吗?这类事情)。

到目前为止,问题在于精度。我通过创建和绘制所有六边形来绘制六边形网格(这甚至听起来对我来说很糟糕……考虑性能)。我使用了一些公式来计算每个六边形的顶点并从中创建多边形。我希望两个连续的六边形的边恰好在同一位置重合,但是四舍五入似乎有点符合我的要求,因为有时六边形的边在同一位置完全匹配(良好),有时它们不匹配。似乎有1个像素的差异(不好)。这给网格的视觉印象很差。也许我对自己的解释不是很好...最好给我代码,然后由您自己运行

总结一下:


您认为我的方法会给将来的性能带来问题吗?
为什么六边形不能正确放置,以使它们共享边?如何避免这个问题?


代码:

#!/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() 


最后但并非最不重要的一点,很长的遗憾:)

谢谢
胜利者

请您参考如下方法:

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

技巧是选择缩放级别,以使(垂直)六边形的宽度是2的倍数,而高度是4的倍数,其宽度/高度大约为sqrt(3/4)。六边形在任一方向上都被轻微挤压,但是对于所有六边形,直径至少为八个像素,效果是不可察觉的。

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



如果六角形的每一边都是a,则h=a/2和w=a*sqrt(3)/2,因此是w/h=sqrt(3)。

为了获得最佳显示质量,请选择整数w和h,以使它们的比率大约为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) 


每三行包含一个带有对角线边界的矩形网格单元,但不必担心:如果边界是 \(位于图片上方),则只需检查是否

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


找出您是否在右上三角部分。如果边界是 / wrt。上图,您只需要检查

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


找出您是否位于右下三角部分。

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

如果您想确定例如鼠标光标相对于其所悬停的六边形的位置,请首先使用上面的内容确定鼠标悬停在哪个六边形上,然后从鼠标坐标中减去该六边形的坐标以获取相对于当前六边形的坐标。