├── .gitignore
├── .idea
├── .gitignore
├── inspectionProfiles
│ ├── Project_Default.xml
│ └── profiles_settings.xml
├── misc.xml
├── modules.xml
├── pyqt_bounding_box.iml
└── vcs.xml
├── LICENSE
├── README.md
├── pyqt_bounding_box
├── __init__.py
└── boundingBox.py
└── setup.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # 디폴트 무시된 파일
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/pyqt_bounding_box.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Jung Gyu Yoon
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # pyqt-bounding-box
2 | PyQt bounding box for graphic design software
3 |
4 | ## Requirements
5 | PyQt5 >= 5.8
6 |
7 | ## Setup
8 | `python -m pip install pyqt-bounding-box`
9 |
10 | ## Feature
11 | * Cursor shape changes properly for position (horizontal/vertical edge, etc.)
12 | * Being able to resize the box horizontally/vertically/diagonally
13 | * Being able to move the box with either mouse cursor or arrow keys
14 | * Being able to change the attribute of the box
15 |
16 | ## Methods Overview
17 | * setLineWidth(self, n: int) - Default width is 3
18 | * setColor(self, color: QColor) - Default color is black
19 | * setStyle(self, style: Qt.PenStyle) - Default style is Qt.DashLine (You can see more about this style in here)
20 | * setWidth(width: int)
21 | * setHeight(height: int)
22 | * setSize(width: int, height: int)
23 |
24 | You can use the standard function like `setPen(pen: QPen)` if you know how to use it, Here's the example:
25 |
26 | ```python
27 | item = BoundingBox()
28 |
29 | pen = QPen()
30 | pen.setStyle(Qt.DashLine)
31 | pen.setWidth(3)
32 | pen.setColor(QColor(0, 0, 0))
33 |
34 | item.setPen(pen)
35 | ```
36 |
37 | ## Example
38 | Code Sample
39 |
40 | ```python
41 | from PyQt5.QtWidgets import QWidget, QGraphicsView, QVBoxLayout, QApplication, QGraphicsScene
42 |
43 | from pyqt_bounding_box.boundingBox import BoundingBox
44 |
45 |
46 | class Example(QWidget):
47 | def __init__(self):
48 | super().__init__()
49 | self.__initUi()
50 |
51 | def __initUi(self):
52 | view = QGraphicsView()
53 | self.__scene = QGraphicsScene()
54 | self.__scene.setSceneRect(0, 0, 400, 400)
55 |
56 | item = BoundingBox()
57 | # item.setLineWidth(8) If you want to change the edge line width, add the code.
58 | # item.setColor(QColor(255, 255, 255)) If you want to change the color of the line to white, add the code.
59 | # item.setStyle(Qt.SolidLine) If you want to change the style of line from dashed to solid line, add the code.
60 | self.__scene.addItem(item)
61 | view.setScene(self.__scene)
62 |
63 | lay = QVBoxLayout()
64 | lay.addWidget(view)
65 |
66 | self.setLayout(lay)
67 |
68 |
69 | if __name__ == "__main__":
70 | import sys
71 |
72 | app = QApplication(sys.argv)
73 | example = Example()
74 | example.show()
75 | app.exec_()
76 | ```
77 |
78 | Result
79 |
80 | https://user-images.githubusercontent.com/55078043/148708740-cd1f0765-7768-44b6-88bb-770e2d34fe12.mp4
81 |
82 | ## See Also
83 | * pyqt-hbounding-box
84 | * pyqt-vbounding-box
85 |
--------------------------------------------------------------------------------
/pyqt_bounding_box/__init__.py:
--------------------------------------------------------------------------------
1 | from .boundingBox import BoundingBox
--------------------------------------------------------------------------------
/pyqt_bounding_box/boundingBox.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtCore import *
2 | from PyQt5.QtGui import *
3 | from PyQt5.QtWidgets import *
4 |
5 |
6 | class BoundingBox(QGraphicsRectItem):
7 | def __init__(self):
8 | super().__init__()
9 | self.__resizeEnabled = False
10 | self.__resize_square_f = False
11 | self.__line_width = 3
12 |
13 | self.__default_width = 200.0
14 | self.__default_height = 200.0
15 |
16 | self.__min_width = 30
17 | self.__min_height = 30
18 |
19 | self.__cursor = QCursor()
20 |
21 | self.__initPosition()
22 | self.__initUi()
23 |
24 | def __initUi(self):
25 | self.setAcceptHoverEvents(True)
26 | self.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsFocusable)
27 | self.__setStyleOfBoundingBox()
28 |
29 | # init the edge direction for set correct reshape cursor based on it
30 | def __initPosition(self):
31 | self.__top = False
32 | self.__bottom = False
33 | self.__left = False
34 | self.__right = False
35 |
36 | # TODO need more refactoring
37 | # This is for preventing setting the width and height smaller than minimum size of each
38 | def __isAbleToSetTop(self, rect, y):
39 | return rect.bottom() - y > self.__min_height
40 | def __isAbleToSetBottom(self, y):
41 | return y > self.__min_height
42 | def __isAbleToSetLeft(self, rect, x):
43 | return rect.right() - x > self.__min_width
44 | def __isAbleToSetRight(self, x):
45 | return x > self.__min_width
46 |
47 | def __setStyleOfBoundingBox(self):
48 | pen = QPen()
49 | pen.setStyle(Qt.DashLine)
50 | pen.setWidth(self.__line_width)
51 | self.setRect(QRectF(0.0, 0.0, self.__default_width, self.__default_height))
52 | self.setPen(pen)
53 |
54 | def __setCursorShapeForCurrentPoint(self, p):
55 | # allow mouse cursor to change shape for scale more easily
56 | rect = self.rect()
57 | rect.setX(self.rect().x() + self.__line_width)
58 | rect.setY(self.rect().y() + self.__line_width)
59 | rect.setWidth(self.rect().width() - self.__line_width * 2)
60 | rect.setHeight(self.rect().height() - self.__line_width * 2)
61 |
62 | if rect.contains(p):
63 | # move
64 | self.setFlags(self.flags() | QGraphicsItem.ItemIsMovable)
65 | self.__cursor.setShape(Qt.SizeAllCursor)
66 | self.setCursor(self.__cursor)
67 | self.__cursor = self.cursor()
68 | self.__resizeEnabled = False
69 | self.__initPosition()
70 | else:
71 | # scale
72 | x = p.x()
73 | y = p.y()
74 |
75 | def setResizeEnabled():
76 | self.setFlags(self.flags() & ~QGraphicsItem.ItemIsMovable)
77 | self.setCursor(self.__cursor)
78 | self.__resizeEnabled = True
79 |
80 | x1 = self.rect().x()
81 | y1 = self.rect().y()
82 | x2 = self.rect().width()
83 | y2 = self.rect().height()
84 |
85 | self.__left = abs(x - x1) <= self.__line_width # if mouse cursor is at the almost far left
86 | self.__top = abs(y - y1) <= self.__line_width # far top
87 | self.__right = abs(x - (x2 + x1)) <= self.__line_width # far right
88 | self.__bottom = abs(y - (y2 + y1)) <= self.__line_width # far bottom
89 |
90 | # set the cursor shape based on flag above
91 | if self.__top or self.__left or self.__bottom or self.__right:
92 | if self.__top and self.__left:
93 | self.__cursor.setShape(Qt.SizeFDiagCursor)
94 | elif self.__top and self.__right:
95 | self.__cursor.setShape(Qt.SizeBDiagCursor)
96 | elif self.__bottom and self.__left:
97 | self.__cursor.setShape(Qt.SizeBDiagCursor)
98 | elif self.__bottom and self.__right:
99 | self.__cursor.setShape(Qt.SizeFDiagCursor)
100 | elif self.__left:
101 | self.__cursor.setShape(Qt.SizeHorCursor)
102 | elif self.__top:
103 | self.__cursor.setShape(Qt.SizeVerCursor)
104 | elif self.__right:
105 | self.__cursor.setShape(Qt.SizeHorCursor)
106 | elif self.__bottom:
107 | self.__cursor.setShape(Qt.SizeVerCursor)
108 | setResizeEnabled()
109 |
110 | def mouseMoveEvent(self, e):
111 | if self.__resizeEnabled:
112 | rect = self.rect()
113 | p = e.pos()
114 | x = p.x()
115 | y = p.y()
116 |
117 | if self.__resize_square_f:
118 | # TODO i'm still working on this, refactoring is necessary
119 | # get the average of width and height
120 | size = p.manhattanLength() // 2
121 | p = QPoint(int(size), int(size))
122 | if x < 0 or y < 0:
123 | p = QPoint(int(size)*-1, int(size)*-1)
124 | if self.__cursor.shape() == Qt.SizeFDiagCursor:
125 | if self.__top and self.__left and self.__isAbleToSetTop(rect, y) and self.__isAbleToSetLeft(rect, x):
126 | rect.setTopLeft(p)
127 | elif self.__bottom and self.__right and self.__isAbleToSetBottom(y) and self.__isAbleToSetRight(x):
128 | rect.setBottomRight(p)
129 | else:
130 | if self.__cursor.shape() == Qt.SizeHorCursor:
131 | if self.__left and self.__isAbleToSetLeft(rect, x):
132 | rect.setLeft(x)
133 | elif self.__right and self.__isAbleToSetRight(x):
134 | rect.setRight(x)
135 | elif self.__cursor.shape() == Qt.SizeVerCursor:
136 | if self.__top and self.__isAbleToSetTop(rect, y):
137 | rect.setTop(y)
138 | elif self.__bottom and self.__isAbleToSetBottom(y):
139 | rect.setBottom(y)
140 | elif self.__cursor.shape() == Qt.SizeBDiagCursor:
141 | if self.__top and self.__right and self.__isAbleToSetTop(rect, y) and self.__isAbleToSetRight(x):
142 | rect.setTopRight(p)
143 | elif self.__bottom and self.__left and self.__isAbleToSetBottom(y) and self.__isAbleToSetLeft(rect, x):
144 | rect.setBottomLeft(p)
145 | elif self.__cursor.shape() == Qt.SizeFDiagCursor:
146 | if self.__top and self.__left and self.__isAbleToSetTop(rect, y) and self.__isAbleToSetLeft(rect, x):
147 | rect.setTopLeft(p)
148 | elif self.__bottom and self.__right and self.__isAbleToSetBottom(y) and self.__isAbleToSetRight(x):
149 | rect.setBottomRight(p)
150 |
151 | self.setRect(rect)
152 |
153 | return super().mouseMoveEvent(e)
154 |
155 | def hoverMoveEvent(self, e):
156 | p = e.pos()
157 |
158 | if self.boundingRect().contains(p) or self.rect().contains(p):
159 | self.__setCursorShapeForCurrentPoint(p)
160 |
161 | return super().hoverMoveEvent(e)
162 |
163 | # moving with arrow keys
164 | def keyPressEvent(self, e):
165 | tr = self.transform()
166 | if e.key() == Qt.Key_Up:
167 | tr.translate(0, -1)
168 | if e.key() == Qt.Key_Down:
169 | tr.translate(0, 1)
170 | if e.key() == Qt.Key_Left:
171 | tr.translate(-1, 0)
172 | if e.key() == Qt.Key_Right:
173 | tr.translate(1, 0)
174 | self.setTransform(tr)
175 | return super().keyPressEvent(e)
176 |
177 | def setLineWidth(self, n: int):
178 | self.__line_width = n
179 | self.__setStyleOfBoundingBox()
180 |
181 | def setColor(self, color: QColor):
182 | pen = self.pen()
183 | pen.setColor(color)
184 | self.setPen(pen)
185 |
186 | # https: // doc.qt.io / qt - 6 / qt.html # PenStyle-enum
187 | def setStyle(self, style: Qt.PenStyle):
188 | pen = self.pen()
189 | pen.setStyle(style)
190 | self.setPen(pen)
191 |
192 | def setWidth(self, width: int):
193 | rect = self.rect()
194 | rect.setWidth(width)
195 | self.setRect(rect)
196 |
197 | def setHeight(self, height: int):
198 | rect = self.rect()
199 | rect.setHeight(height)
200 | self.setRect(rect)
201 |
202 | def setSize(self, width: int, height: int):
203 | rect = self.rect()
204 | rect.setSize(QSizeF(width, height))
205 | self.setRect(rect)
206 |
207 | def setResizeAsSquare(self, f: bool):
208 | self.__resize_square_f = f
209 |
210 | def setMinimumSize(self, width, height):
211 | self.__min_width = width
212 | self.__min_height = height
213 | # TODO set width or height of the current rect larger than minimum size after at least one of them is set by
214 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import codecs
2 | import os
3 |
4 | from setuptools import setup, find_packages
5 |
6 | here = os.path.abspath(os.path.dirname(__file__))
7 |
8 | with codecs.open(os.path.join(here, "README.md"), encoding="utf-8") as fh:
9 | long_description = "\n" + fh.read()
10 |
11 |
12 | setup(
13 | name='pyqt-bounding-box',
14 | version='0.0.16',
15 | author='Jung Gyu Yoon',
16 | author_email='yjg30737@gmail.com',
17 | license='MIT',
18 | packages=find_packages(),
19 | description='PyQt bounding box for graphic design software',
20 | url='https://github.com/yjg30737/pyqt-bounding-box.git',
21 | long_description_content_type='text/markdown',
22 | long_description=long_description,
23 | install_requires=[
24 | 'PyQt5>=5.8'
25 | ]
26 | )
--------------------------------------------------------------------------------