如何在 Python 中绘制网格和矩形?

发布于 2024-09-15 09:31:52 字数 204 浏览 17 评论 0原文

您可以轻松地使用哪些 Python 相关代码(PyGTK、Glade、Tkinter、PyQT、wxPython、Cairo 等)来创建 GUI 来执行以下部分或全部操作?

  1. GUI 的一部分有一个不可移动的方形网格。
  2. 用户可以按下按钮来创建可调整大小的矩形。
  3. 用户可以将矩形拖动到网格上的任何位置,它将捕捉到网格。

What Python-related code (PyGTK, Glade, Tkinter, PyQT, wxPython, Cairo, ...) could you easily use to create a GUI to do some or all of the following?

  1. Part of the GUI has an immovable square grid.
  2. The user can press a button to create a resizable rectangle.
  3. The user can drag the rectangle anywhere on the grid, and it will snap to the grid.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

陌路黄昏 2024-09-22 09:31:52

PyQt 附带的DiagramScene Eaxmple 实现了您想要的大部分功能。它有一个固定的背景网格,您可以创建一个矩形对象,但它不能调整大小,也不能捕捉到网格。

这篇SO文章提供了有关使用鼠标调整图形对象大小的建议。它适用于 C++ Qt,但该技术应该很容易在 PyQt 中复制。

对于对齐网格,我认为没有任何内置功能。您可能需要重新实现 itemChange(GraphicsItemChange change, const QVariant &value) 函数。伪代码:

if (object not possitioned exactly on the grid):
    (possition the item on the grid)

重新定位该项目将导致 itemChange 再次被调用,但这没关系,因为该项目将被正确定位并且不会再次移动,因此您不会陷入无限循环。

The DiagramScene Eaxmple that comes with PyQt implements much of the functionality you want. It has a fixed background grid, you can create a rectangle object but it's not resizable and doesn't snap to grid.

This SO article has advice on resizing graphical objects with the mouse. It's for C++ Qt but the technique should be easy to replicate in PyQt.

For snap-to-grid I don't think there is any built-in functionality. You would probably need to reimplement the itemChange(GraphicsItemChange change, const QVariant &value) function. Pseudocode:

if (object not possitioned exactly on the grid):
    (possition the item on the grid)

Repossitioning the item will cause itemChange to get called again, but that's ok because the item will be possitioned correctly and won't be moved again, so you'll not be stuck in an endless loop.

ぃ双果 2024-09-22 09:31:52

我一直在寻找这样的东西一段时间,最后设法用 Python wx 编写了一个“最小”的工作示例,利用 wx.lib.ogl 及其 < code>Diagram 和 ShapeCanvas 类。代码(如下)的结果如下:

test.png

注意:

  • 应用程序以添加的圆圈开始;按空格在随机位置添加矩形
  • 单击一个对象以选择它(以显示手柄);要取消选择它,请再次单击该对象(单击背景无效) - 这是 ogl 的功能
  • 网格是“手动”绘制的;然而,对齐网格是 ogl 的功能,
  • 对齐网格仅在通过鼠标拖动移动形状时自动工作;出于其他目的,您必须手动将其称为
  • 对齐网格 - 以及通过手柄调整形状大小 - 相对于每个形状的中心起作用(不确定 ogl 是否允许将该锚点更改为,例如,左下角)

该示例使用 MyPanel 类进行自己的绘图,并且继承自 ogl.ShapeCanvaswx.Panel code> (尽管可以删除带有 wx.Panel 的 mixin,并且代码仍然会以相同的方式工作) - 然后将其添加到 wx.Frame 中。请注意一些警告的代码注释(例如使用 ogl.ShapeCanvas 阻止所有按键事件,除非首先对该小部件执行 SetFocus)。

代码:

import wx
import wx.lib.ogl as ogl
import random

# tested on wxPython 2.8.11.0, Python 2.7.1+, Ubuntu 11.04

# started from:
# http://stackoverflow.com/questions/25756896/drawing-to-panel-inside-of-frame-in-wxpython/27804975#27804975

# see also:
# wxPython-2.8.11.0-demo/demo/OGL.py
# https://www.daniweb.com/software-development/python/threads/186203/creating-editable-drawing-objects-in-wxpython
# http://gscept.com/svn/Docs/PSE/Milestone%203/code/trunk/python_test/src/oglEditor.py
# http://nullege.com/codes/search/wx.lib.ogl.Diagram
# http://nullege.com/codes/show/src%40w%40e%40web2cms-HEAD%40web2py%40gluon%40contrib%40pyfpdf%40designer.py/465/wx.lib.ogl.Diagram/python
# https://www.daniweb.com/software-development/python/threads/204969/setfocus-on-canvas-not-working
# http://stackoverflow.com/questions/3538769/how-do-you-draw-a-grid-and-rectangles-in-python
# http://stackoverflow.com/questions/7794496/snapping-to-pixels-in-wxpython


# ogl.ShapeCanvas must go first, else TypeError:  Cannot create a consistent method resolution
class MyPanel(ogl.ShapeCanvas, wx.Panel):#(wx.PyPanel): #PyPanel also works
  def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize, style=0, name="MyPanel"):
    super(MyPanel, self).__init__(parent, id, pos, size, style, name)
    self.gridsize = 20 # in pixels
    # must have these (w. Diagram) if using ogl.ShapeCanvas:
    self.diagram = ogl.Diagram()
    self.SetDiagram(self.diagram)
    self.diagram.SetCanvas(self)
    # set up snap to grid - note, like this it works only for drag (relative to shape center), not for resize via handles!
    self.diagram.SetGridSpacing( self.gridsize )
    self.diagram.SetSnapToGrid( True )
    # initialize array of shapes with one element
    self.shapes = []
    self.MyAddShape(
      ogl.CircleShape(85), # diameter - drag marquee will not be visible if (diameter mod gridsize == 0), as it will overlap with the grid lines
      60, 60, wx.Pen(wx.BLUE, 3), wx.GREEN_BRUSH, "Circle"
      )
    self.Bind(wx.EVT_SIZE, self.OnSize)
    self.Bind(wx.EVT_PAINT, self.OnPaint)
    wx.EVT_KEY_DOWN(self, self.OnKeyPressedM)
  def OnKeyPressedM(self, event):
    keyCode = event.GetKeyCode()
    print("MyPanel.OnKeyPressedM: %d"%(keyCode) )
    # insert a rectangle here on [SPACE]:
    if keyCode == wx.WXK_SPACE:
      randx = random.randint(1, 300)
      randy = random.randint(1, 200)
      if self.diagram.GetSnapToGrid():
        randx, randy = self.Snap(randx, randy) # must do snapping (if desired) manually, here at insertion!
      self.MyAddShape(
          ogl.RectangleShape(60, 20),
          randx, randy, wx.BLACK_PEN, wx.LIGHT_GREY_BRUSH, "Rect %d"%(len(self.shapes))
        )
      self.Refresh(False)
    event.Skip() # must have this, to have the MyFrame.OnKeyPressed trigger as well!
  def OnSize(self, event):
    #print("OnSize" +str(event))
    self.Refresh() # must have here!
    event.Skip()
  def DrawBackgroundGrid(self):
    dc = wx.PaintDC(self)
    #print(dc)
    rect = self.GetClientRect()
    rx, ry, rw, rh = rect
    dc.SetBrush(wx.Brush(self.GetForegroundColour()))
    dc.SetPen(wx.Pen(self.GetForegroundColour()))
    # draw ("tile") the grid
    x = rx
    while x < rx+rw:
      y = ry
      dc.DrawLine(x, ry, x, ry+rh) # long (vertical) lines
      while y < ry+rh:
        dc.DrawLine(x, y, x+self.gridsize, y) # short (horizontal) lines
        y = y + self.gridsize
      x = x + self.gridsize
  def OnPaint(self, event):
    dc = wx.PaintDC(self) # works
    self.DrawBackgroundGrid()
    # self.Refresh() # recurses here - don't use!
    # self.diagram.GetCanvas().Refresh() # blocks here - don't use!
    self.diagram.GetCanvas().Redraw(dc) # this to redraw the elements on top of the grid, drawn just before
  # MyAddShape is from OGL.py:
  def MyAddShape(self, shape, x, y, pen, brush, text):
    # Composites have to be moved for all children to get in place
    if isinstance(shape, ogl.CompositeShape):
      dc = wx.ClientDC(self)
      self.PrepareDC(dc)
      shape.Move(dc, x, y)
    else:
      shape.SetDraggable(True, True)
    shape.SetCanvas(self)
    shape.SetX(x)
    shape.SetY(y)
    if pen:  shape.SetPen(pen)
    if brush:  shape.SetBrush(brush)
    if text:
      for line in text.split('\n'):
        shape.AddText(line)
    #shape.SetShadowMode(ogl.SHADOW_RIGHT)
    self.diagram.AddShape(shape)
    shape.Show(True)
    evthandler = MyEvtHandler(self)
    evthandler.SetShape(shape)
    evthandler.SetPreviousHandler(shape.GetEventHandler())
    shape.SetEventHandler(evthandler)
    self.shapes.append(shape)
    return shape

# copyfrom OGL.pyl; modded
class MyEvtHandler(ogl.ShapeEvtHandler):
  def __init__(self, parent): #
    ogl.ShapeEvtHandler.__init__(self)
    self.parent = parent
  def UpdateStatusBar(self, shape):
    x, y = shape.GetX(), shape.GetY()
    width, height = shape.GetBoundingBoxMax()
    self.parent.Refresh(False) # do here, to redraw the background after a drag move, or scale of shape
    print("Pos: (%d, %d)  Size: (%d, %d)" % (x, y, width, height))
  def OnLeftClick(self, x, y, keys=0, attachment=0):
    # note: to deselect a selected shape, don't click the background, but click the shape again
    shape = self.GetShape()
    canvas = shape.GetCanvas()
    dc = wx.ClientDC(canvas)
    canvas.PrepareDC(dc)
    if shape.Selected():
      shape.Select(False, dc)
      #canvas.Redraw(dc)
      canvas.Refresh(False)
    else:
      redraw = False
      shapeList = canvas.GetDiagram().GetShapeList()
      toUnselect = []
      for s in shapeList:
        if s.Selected():
          # If we unselect it now then some of the objects in
          # shapeList will become invalid (the control points are
          # shapes too!) and bad things will happen...
          toUnselect.append(s)
      shape.Select(True, dc)
      if toUnselect:
        for s in toUnselect:
          s.Select(False, dc)
        ##canvas.Redraw(dc)
        canvas.Refresh(False)
    self.UpdateStatusBar(shape)
  def OnEndDragLeft(self, x, y, keys=0, attachment=0):
    shape = self.GetShape()
    ogl.ShapeEvtHandler.OnEndDragLeft(self, x, y, keys, attachment)
    if not shape.Selected():
      self.OnLeftClick(x, y, keys, attachment)
    self.UpdateStatusBar(shape)
  def OnSizingEndDragLeft(self, pt, x, y, keys, attch):
    ogl.ShapeEvtHandler.OnSizingEndDragLeft(self, pt, x, y, keys, attch)
    self.UpdateStatusBar(self.GetShape())
  def OnMovePost(self, dc, x, y, oldX, oldY, display):
    shape = self.GetShape()
    ogl.ShapeEvtHandler.OnMovePost(self, dc, x, y, oldX, oldY, display)
    self.UpdateStatusBar(shape)
    if "wxMac" in wx.PlatformInfo:
      shape.GetCanvas().Refresh(False)
  def OnRightClick(self, *dontcare):
    #self.log.WriteText("%s\n" % self.GetShape())
    print("OnRightClick")

class MyFrame(wx.Frame):
  def __init__(self, parent):
    wx.Frame.__init__(self, parent, -1, "Custom Panel Grid Demo")
    # This creates some pens and brushes that the OGL library uses.
    # (else "global name 'BlackForegroundPen' is not defined")
    # It should be called after the app object has been created, but
    # before OGL is used.
    ogl.OGLInitialize()
    self.SetSize((300, 200))
    self.panel = MyPanel(self) #wx.Panel(self)
    self.panel.SetBackgroundColour(wx.Colour(250,250,250))
    self.panel.SetForegroundColour(wx.Colour(127,127,127))
    sizer_1 = wx.BoxSizer(wx.HORIZONTAL)
    sizer_1.Add(self.panel, 1, wx.EXPAND | wx.ALL, 0)
    self.SetSizer(sizer_1)
    self.SetAutoLayout(1)
    self.Layout()
    self.Show(1)
    # NOTE: on my dev versions, using ogl.Diagram causes _all_
    # key press events, from *anywhere*, to stop propagating!
    # Doing a .SetFocus on the ogl.ShapeCanvas panel,
    # finally makes the Key events propagate!
    # (troubleshoot via run.py from wx python demo)
    self.panel.SetFocus()
    self.Bind(wx.EVT_CHAR_HOOK, self.OnKeyPressed) # EVT_CHAR_HOOK EVT_KEY_DOWN
  def OnKeyPressed(self, event):
    print("MyFrame.OnKeyPressed (just testing)")

app = wx.App(0)
frame = MyFrame(None)
app.SetTopWindow(frame)
frame.Show()
app.MainLoop()

I was looking for a while for something like this, and finally managed to cook up a "minimal" working example with Python wx, utilizing wx.lib.ogl and its Diagram and ShapeCanvas classes. The code (below) results with something like this:

test.png

Note:

  • The app starts with the circle added; press SPACE to add rectangles at random position
  • Click an object to select it (to show handles); to deselect it, click the object again (clicking the background has no effect) - this is functionality of ogl
  • The grid is drawn "manually"; however the snapping-to-grid is functionality of ogl
  • Snap-to-grid only works automatically when moving shapes with mouse drag; for other purposes you must manually call it
  • Snap-to-grid - as well as resizing of shape by handles - works in respect to the center of each shape (not sure if ogl allows for changing that anchor to, say, bottom left corner)

The example uses a MyPanel class that does its own drawing, and inherits both from ogl.ShapeCanvas and from wx.Panel (though the mixin with wx.Panel can be dropped, and the code will still work the same) - which is then added to a wx.Frame. Note the code comments for some caveats (such as the use of ogl.ShapeCanvas blocking all key events, unless a SetFocus is performed on that widget first).

The code:

import wx
import wx.lib.ogl as ogl
import random

# tested on wxPython 2.8.11.0, Python 2.7.1+, Ubuntu 11.04

# started from:
# http://stackoverflow.com/questions/25756896/drawing-to-panel-inside-of-frame-in-wxpython/27804975#27804975

# see also:
# wxPython-2.8.11.0-demo/demo/OGL.py
# https://www.daniweb.com/software-development/python/threads/186203/creating-editable-drawing-objects-in-wxpython
# http://gscept.com/svn/Docs/PSE/Milestone%203/code/trunk/python_test/src/oglEditor.py
# http://nullege.com/codes/search/wx.lib.ogl.Diagram
# http://nullege.com/codes/show/src%40w%40e%40web2cms-HEAD%40web2py%40gluon%40contrib%40pyfpdf%40designer.py/465/wx.lib.ogl.Diagram/python
# https://www.daniweb.com/software-development/python/threads/204969/setfocus-on-canvas-not-working
# http://stackoverflow.com/questions/3538769/how-do-you-draw-a-grid-and-rectangles-in-python
# http://stackoverflow.com/questions/7794496/snapping-to-pixels-in-wxpython


# ogl.ShapeCanvas must go first, else TypeError:  Cannot create a consistent method resolution
class MyPanel(ogl.ShapeCanvas, wx.Panel):#(wx.PyPanel): #PyPanel also works
  def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize, style=0, name="MyPanel"):
    super(MyPanel, self).__init__(parent, id, pos, size, style, name)
    self.gridsize = 20 # in pixels
    # must have these (w. Diagram) if using ogl.ShapeCanvas:
    self.diagram = ogl.Diagram()
    self.SetDiagram(self.diagram)
    self.diagram.SetCanvas(self)
    # set up snap to grid - note, like this it works only for drag (relative to shape center), not for resize via handles!
    self.diagram.SetGridSpacing( self.gridsize )
    self.diagram.SetSnapToGrid( True )
    # initialize array of shapes with one element
    self.shapes = []
    self.MyAddShape(
      ogl.CircleShape(85), # diameter - drag marquee will not be visible if (diameter mod gridsize == 0), as it will overlap with the grid lines
      60, 60, wx.Pen(wx.BLUE, 3), wx.GREEN_BRUSH, "Circle"
      )
    self.Bind(wx.EVT_SIZE, self.OnSize)
    self.Bind(wx.EVT_PAINT, self.OnPaint)
    wx.EVT_KEY_DOWN(self, self.OnKeyPressedM)
  def OnKeyPressedM(self, event):
    keyCode = event.GetKeyCode()
    print("MyPanel.OnKeyPressedM: %d"%(keyCode) )
    # insert a rectangle here on [SPACE]:
    if keyCode == wx.WXK_SPACE:
      randx = random.randint(1, 300)
      randy = random.randint(1, 200)
      if self.diagram.GetSnapToGrid():
        randx, randy = self.Snap(randx, randy) # must do snapping (if desired) manually, here at insertion!
      self.MyAddShape(
          ogl.RectangleShape(60, 20),
          randx, randy, wx.BLACK_PEN, wx.LIGHT_GREY_BRUSH, "Rect %d"%(len(self.shapes))
        )
      self.Refresh(False)
    event.Skip() # must have this, to have the MyFrame.OnKeyPressed trigger as well!
  def OnSize(self, event):
    #print("OnSize" +str(event))
    self.Refresh() # must have here!
    event.Skip()
  def DrawBackgroundGrid(self):
    dc = wx.PaintDC(self)
    #print(dc)
    rect = self.GetClientRect()
    rx, ry, rw, rh = rect
    dc.SetBrush(wx.Brush(self.GetForegroundColour()))
    dc.SetPen(wx.Pen(self.GetForegroundColour()))
    # draw ("tile") the grid
    x = rx
    while x < rx+rw:
      y = ry
      dc.DrawLine(x, ry, x, ry+rh) # long (vertical) lines
      while y < ry+rh:
        dc.DrawLine(x, y, x+self.gridsize, y) # short (horizontal) lines
        y = y + self.gridsize
      x = x + self.gridsize
  def OnPaint(self, event):
    dc = wx.PaintDC(self) # works
    self.DrawBackgroundGrid()
    # self.Refresh() # recurses here - don't use!
    # self.diagram.GetCanvas().Refresh() # blocks here - don't use!
    self.diagram.GetCanvas().Redraw(dc) # this to redraw the elements on top of the grid, drawn just before
  # MyAddShape is from OGL.py:
  def MyAddShape(self, shape, x, y, pen, brush, text):
    # Composites have to be moved for all children to get in place
    if isinstance(shape, ogl.CompositeShape):
      dc = wx.ClientDC(self)
      self.PrepareDC(dc)
      shape.Move(dc, x, y)
    else:
      shape.SetDraggable(True, True)
    shape.SetCanvas(self)
    shape.SetX(x)
    shape.SetY(y)
    if pen:  shape.SetPen(pen)
    if brush:  shape.SetBrush(brush)
    if text:
      for line in text.split('\n'):
        shape.AddText(line)
    #shape.SetShadowMode(ogl.SHADOW_RIGHT)
    self.diagram.AddShape(shape)
    shape.Show(True)
    evthandler = MyEvtHandler(self)
    evthandler.SetShape(shape)
    evthandler.SetPreviousHandler(shape.GetEventHandler())
    shape.SetEventHandler(evthandler)
    self.shapes.append(shape)
    return shape

# copyfrom OGL.pyl; modded
class MyEvtHandler(ogl.ShapeEvtHandler):
  def __init__(self, parent): #
    ogl.ShapeEvtHandler.__init__(self)
    self.parent = parent
  def UpdateStatusBar(self, shape):
    x, y = shape.GetX(), shape.GetY()
    width, height = shape.GetBoundingBoxMax()
    self.parent.Refresh(False) # do here, to redraw the background after a drag move, or scale of shape
    print("Pos: (%d, %d)  Size: (%d, %d)" % (x, y, width, height))
  def OnLeftClick(self, x, y, keys=0, attachment=0):
    # note: to deselect a selected shape, don't click the background, but click the shape again
    shape = self.GetShape()
    canvas = shape.GetCanvas()
    dc = wx.ClientDC(canvas)
    canvas.PrepareDC(dc)
    if shape.Selected():
      shape.Select(False, dc)
      #canvas.Redraw(dc)
      canvas.Refresh(False)
    else:
      redraw = False
      shapeList = canvas.GetDiagram().GetShapeList()
      toUnselect = []
      for s in shapeList:
        if s.Selected():
          # If we unselect it now then some of the objects in
          # shapeList will become invalid (the control points are
          # shapes too!) and bad things will happen...
          toUnselect.append(s)
      shape.Select(True, dc)
      if toUnselect:
        for s in toUnselect:
          s.Select(False, dc)
        ##canvas.Redraw(dc)
        canvas.Refresh(False)
    self.UpdateStatusBar(shape)
  def OnEndDragLeft(self, x, y, keys=0, attachment=0):
    shape = self.GetShape()
    ogl.ShapeEvtHandler.OnEndDragLeft(self, x, y, keys, attachment)
    if not shape.Selected():
      self.OnLeftClick(x, y, keys, attachment)
    self.UpdateStatusBar(shape)
  def OnSizingEndDragLeft(self, pt, x, y, keys, attch):
    ogl.ShapeEvtHandler.OnSizingEndDragLeft(self, pt, x, y, keys, attch)
    self.UpdateStatusBar(self.GetShape())
  def OnMovePost(self, dc, x, y, oldX, oldY, display):
    shape = self.GetShape()
    ogl.ShapeEvtHandler.OnMovePost(self, dc, x, y, oldX, oldY, display)
    self.UpdateStatusBar(shape)
    if "wxMac" in wx.PlatformInfo:
      shape.GetCanvas().Refresh(False)
  def OnRightClick(self, *dontcare):
    #self.log.WriteText("%s\n" % self.GetShape())
    print("OnRightClick")

class MyFrame(wx.Frame):
  def __init__(self, parent):
    wx.Frame.__init__(self, parent, -1, "Custom Panel Grid Demo")
    # This creates some pens and brushes that the OGL library uses.
    # (else "global name 'BlackForegroundPen' is not defined")
    # It should be called after the app object has been created, but
    # before OGL is used.
    ogl.OGLInitialize()
    self.SetSize((300, 200))
    self.panel = MyPanel(self) #wx.Panel(self)
    self.panel.SetBackgroundColour(wx.Colour(250,250,250))
    self.panel.SetForegroundColour(wx.Colour(127,127,127))
    sizer_1 = wx.BoxSizer(wx.HORIZONTAL)
    sizer_1.Add(self.panel, 1, wx.EXPAND | wx.ALL, 0)
    self.SetSizer(sizer_1)
    self.SetAutoLayout(1)
    self.Layout()
    self.Show(1)
    # NOTE: on my dev versions, using ogl.Diagram causes _all_
    # key press events, from *anywhere*, to stop propagating!
    # Doing a .SetFocus on the ogl.ShapeCanvas panel,
    # finally makes the Key events propagate!
    # (troubleshoot via run.py from wx python demo)
    self.panel.SetFocus()
    self.Bind(wx.EVT_CHAR_HOOK, self.OnKeyPressed) # EVT_CHAR_HOOK EVT_KEY_DOWN
  def OnKeyPressed(self, event):
    print("MyFrame.OnKeyPressed (just testing)")

app = wx.App(0)
frame = MyFrame(None)
app.SetTopWindow(frame)
frame.Show()
app.MainLoop()
浅沫记忆 2024-09-22 09:31:52

这些动作并不那么困难。您真正需要的是命中检测,这并不难(光标是否位于正确的区域?好的,然后执行操作)。更困难的部分是为正在使用的工具包找到合适的画布小部件

Those actions are not that difficult. All you really need for that is hit detection, which is not hard (is the cursor over the correct area? Okay, perform the operation then). The harder part is finding an appropriate canvas widget for the toolkit in use.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文