butterfly_viewer.aux_rulers
Ruler items for CustomQGraphicsScene.
Not intended as a script. Used in Butterfly Viewer.
RulerItem creates a movable ruler on QGraphicsScene with specified units of length.
1#!/usr/bin/env python3 2 3"""Ruler items for CustomQGraphicsScene. 4 5Not intended as a script. Used in Butterfly Viewer. 6 7RulerItem creates a movable ruler on QGraphicsScene with specified units of length. 8""" 9# SPDX-License-Identifier: GPL-3.0-or-later 10 11 12 13import sys 14import math 15from PyQt5 import QtWidgets, QtCore, QtGui 16 17 18 19class CustomItem(QtWidgets.QGraphicsEllipseItem): 20 """Create an endpoint for RulerItem and handle change in ruler position. 21 22 Instantiated with a QRectF as input argument. For example: 23 rect = QtCore.QRectF(point_topleft, point_bottomright) 24 endpoint = CustomItem(rect) 25 26 As implemented in RulerItem, the two endpoints reference each other when an endpoint is dragged. 27 This allows the other graphics items of the RulerItem to follow the endpoint movement. 28 """ 29 30 def __init__(self, *args, **kwargs): 31 super().__init__(*args, **kwargs) 32 33 self.setFlag(self.ItemIsMovable) 34 self.setFlag(self.ItemSendsGeometryChanges) 35 self.setFlag(self.ItemIgnoresTransformations) 36 self.line = None 37 self.is_point = None 38 self.text = None 39 self.text1 = None 40 self.text2 = None 41 self.unit = "px" 42 self.px_per_unit = 1.0 43 self.relative_origin_position = "bottomleft" 44 self.y_origin = -1 45 46 def add_line(self, line, is_point): 47 """Add QGraphicsLineItem as reference. 48 49 Args: 50 line (QGraphicsLineItem) 51 is_point (bool): True if endpoint is moving; False if static.""" 52 self.line = line 53 self.is_point = is_point 54 55 def add_text(self, text): 56 """QGraphicsTextItem: Add text item to center of ruler.""" 57 self.text = text 58 59 def add_text1(self, text): 60 """QGraphicsTextItem: Add text item to first endpoint of ruler.""" 61 self.text1 = text 62 63 def add_text2(self, text): 64 """QGraphicsTextItem: Add text item to second endpoint of ruler.""" 65 self.text2 = text 66 67 def set_px_per_unit(self, px_per_unit): 68 """float: Set conversion for pixels to specified unit of length.""" 69 self.px_per_unit = px_per_unit 70 71 def set_unit(self, unit): 72 """str: Set abbreviation for unit of length (for example, "mm" or "px").""" 73 self.unit = unit 74 75 def set_relative_origin_position(self, relative_origin_position): 76 """Set position of origin for coordinates, distances, and angles. 77 78 Two options for relative origin: 79 "topleft" has (0,0) at the top-left pixel of the image, which is typical for graphics 80 systems. One can think of this as a standard XY coordinate system mirrored about the 81 X-axis, where Y increases downwards. This means clockwise rotation is a positive angle. 82 83 "bottomright" has (0,0) at the bottom-left pixel of the image, just like a standard XY 84 coordinate system. This means counter-clockwise rotation is a positive angle. 85 86 Args: 87 relative_origin_position (str): "topleft" or "bottomleft" 88 """ 89 self.relative_origin_position = relative_origin_position 90 if self.relative_origin_position == "topleft": 91 self.y_origin = +1 92 elif self.relative_origin_position == "bottomleft": 93 self.y_origin = -1 94 95 def itemChange(self, change, value): 96 """Extend itemChange to update the positions and texts of the ruler line and labels.""" 97 if change == self.ItemPositionChange and self.scene(): 98 new_pos = value 99 100 self.move_line_to_center(new_pos) 101 102 self.update_text() 103 self.move_text(new_pos) 104 105 self.update_text1() 106 self.move_text1(new_pos) 107 108 self.update_text2() 109 self.move_text2(new_pos) 110 111 return super(CustomItem, self).itemChange(change, value) 112 113 def move_line_to_center(self, new_pos): 114 """QPointF: Set the center of the ruler line to a position.""" 115 x_offset = self.rect().x() + self.rect().width()/2 116 y_offset = self.rect().y() + self.rect().height()/2 117 118 new_center_pos = QtCore.QPointF(new_pos.x()+x_offset, new_pos.y()+y_offset) 119 120 p1 = new_center_pos if self.is_point else self.line.line().p1() 121 p2 = self.line.line().p2() if self.is_point else new_center_pos 122 123 self.line.setLine(QtCore.QLineF(p1, p2)) 124 125 def update_text(self): 126 """Refresh the text of the ruler's center label.""" 127 length_px = self.get_line_length(self.line.line()) 128 unit = self.unit 129 px_per_unit = self.px_per_unit 130 length_unit = length_px/px_per_unit 131 string_length = "{:.1f}".format(length_unit) + " " + unit 132 string = "<div style='background:rgba(0, 0, 0, 91);'>" + string_length + "</div>" 133 self.text.setHtml(string) 134 135 def update_text1(self): 136 """Refresh the text of the ruler's endpoint 1 label.""" 137 length_px = self.get_line_length(self.line.line()) 138 unit = self.unit 139 px_per_unit = self.px_per_unit 140 p1 = self.line.line().p1() 141 p2 = self.line.line().p2() 142 143 length_unit = length_px/px_per_unit 144 dx_unit = (p1.x()-p2.x())/px_per_unit 145 dy_unit = (p1.y()-p2.y())/px_per_unit 146 dy_unit *= self.y_origin 147 ang = math.degrees(math.atan2(dy_unit, dx_unit)) 148 149 string_abs = "|v| " + "{:.1f}".format(length_unit) + " " + unit 150 string_dx = "⬌ " + "{:.1f}".format(dx_unit) + " " + unit 151 string_dy = "⬍ " + "{:.1f}".format(dy_unit) + " " + unit 152 string_ang = "∠ " + "{:.1f}".format(ang) + "°" 153 string = "<div style='background:rgba(0, 0, 0, 91);'>" + string_abs + "<br>" + string_dx + "<br>" + string_dy + "<br>" + string_ang + "</div>" 154 self.text1.setHtml(string) 155 156 def update_text2(self): 157 """Refresh the text of the ruler's endpoint 2 label.""" 158 length_px = self.get_line_length(self.line.line()) 159 unit = self.unit 160 px_per_unit = self.px_per_unit 161 p1 = self.line.line().p1() 162 p2 = self.line.line().p2() 163 164 length_unit = length_px/px_per_unit 165 dx_unit = (p2.x()-p1.x())/px_per_unit 166 dy_unit = (p2.y()-p1.y())/px_per_unit 167 dy_unit *= self.y_origin 168 ang = math.degrees(math.atan2(dy_unit, dx_unit)) 169 170 string_abs = "|v| " + "{:.1f}".format(length_unit) + " " + unit 171 string_dx = "⬌ " + "{:.1f}".format(dx_unit) + " " + unit 172 string_dy = "⬍ " + "{:.1f}".format(dy_unit) + " " + unit 173 string_ang = "∠ " + "{:.1f}".format(ang) + "°" 174 string = "<div style='background:rgba(0, 0, 0, 91);'>" + string_abs + "<br>" + string_dx + "<br>" + string_dy + "<br>" + string_ang + "</div>" 175 self.text2.setHtml(string) 176 177 def move_text(self, new_pos): 178 """QPointF: Set the position of the ruler's center label.""" 179 if self.text: 180 center_pos = self.line.line().center() 181 x_offset = center_pos.x() 182 y_offset = center_pos.y() 183 new_pos = QtCore.QPointF(x_offset, y_offset) 184 self.text.setPos(new_pos) 185 186 def move_text1(self, new_pos): 187 """QPointF: Set the position of the ruler's endpoint 1 label.""" 188 if self.text: 189 pos = self.line.line().p1() 190 x_offset = pos.x() 191 y_offset = pos.y() 192 new_pos = QtCore.QPointF(x_offset, y_offset) 193 self.text1.setPos(new_pos) 194 195 def move_text2(self, new_pos): 196 """QPointF: Set the position of the ruler's endpoint 2 label.""" 197 if self.text: 198 pos = self.line.line().p2() 199 x_offset = pos.x() 200 y_offset = pos.y() 201 new_pos = QtCore.QPointF(x_offset, y_offset) 202 self.text2.setPos(new_pos) 203 204 def refresh_positions(self): 205 """Convenience function to refresh (update) positions of the ruler's line and endpoints.""" 206 self.move_line_to_center(self.pos()) 207 self.move_text(self.pos()) 208 self.update_text() 209 self.update_text1() 210 self.update_text2() 211 212 def get_line_length(self, line): 213 """Calculate the length of a QLineF. 214 215 Args: 216 line (QLineF) 217 218 Returns: 219 length (float) 220 """ 221 dx = line.x2() - line.x1() 222 dy = line.y2() - line.y1() 223 length = math.sqrt(dx**2 + dy**2) 224 return length 225 226 227 228class RulerItem(QtWidgets.QGraphicsRectItem): 229 """Create a movable ruler on QGraphicsScene with a specified unit of length. 230 231 Features: 232 Draggable endpoints. 233 Center label showing absolute length. 234 Endpoint labels showing difference in absolute length, horizontal and vertical delta, and angle. 235 236 Args: 237 unit (str): The text for labeling units of ruler values. 238 px_per_mm (float): The conversion for pixels to millimeters. For example, 10 means 10 239 pixels-per-mm, meaning the ruler value will show 1 mm when measuring 10 pixels. Set to 240 1.0 if the ruler has units of pixels. 241 initial_pos_p1 (QPointF): The position of endpoint 1 on the scene. 242 initial_pos_p2 (QPointF): The position of endpoint 2 on the scene. 243 relative_origin_position (str): The orientation of the origin for coordinate system 244 ("topleft" or "bottomleft"). 245 """ 246 247 def __init__(self, unit = "px", px_per_mm = None, initial_pos_p1=None, initial_pos_p2=None, relative_origin_position="bottomleft"): 248 super().__init__() 249 250 self.unit = unit 251 252 mm_per_unit = 1.0 253 if "cm" == unit: 254 mm_per_unit = 10.0 255 elif "m" == unit: 256 mm_per_unit = 1000.0 257 elif "in" == unit: 258 mm_per_unit = 25.4 259 elif "ft" == unit: 260 mm_per_unit = 304.8 261 elif "yd" == unit: 262 mm_per_unit = 914.4 263 264 if "px" == unit: 265 self.px_per_unit = 1.0 266 else: 267 self.px_per_unit = px_per_mm * mm_per_unit 268 269 self._mm_per_unit = mm_per_unit 270 271 self.relative_origin_position = relative_origin_position 272 273 if not initial_pos_p1: 274 initial_pos_p1 = QtCore.QPointF(10,10) 275 if not initial_pos_p2: 276 initial_pos_p2 = QtCore.QPointF(100,200) 277 278 pen = QtGui.QPen() 279 pen.setWidth(2) 280 pen.setCosmetic(True) 281 pen.setColor(QtCore.Qt.white) # setColor also works 282 pen.setCapStyle(QtCore.Qt.SquareCap) 283 pen.setJoinStyle(QtCore.Qt.MiterJoin) 284 285 brush = QtGui.QBrush() 286 brush.setColor(QtCore.Qt.white) 287 brush.setStyle(QtCore.Qt.SolidPattern) 288 289 brush_black = QtGui.QBrush() 290 brush_black.setColor(QtCore.Qt.black) 291 brush_black.setStyle(QtCore.Qt.SolidPattern) 292 293 294 width = 8 295 height = 8 296 point_topleft = QtCore.QPointF(-width/2, -height/2) 297 point_bottomright = QtCore.QPointF(width/2,height/2) 298 ellipse_rect = QtCore.QRectF(point_topleft, point_bottomright) 299 300 self.ellipse_item1 = CustomItem(ellipse_rect) 301 self.ellipse_item1.setPos(initial_pos_p1) 302 self.ellipse_item1.setBrush(brush_black) 303 self.ellipse_item1.setPen(pen) 304 305 306 text_item = QtWidgets.QGraphicsTextItem("text") 307 text_item.setPos(0,0) 308 font = text_item.font() 309 font.setPointSize(11) 310 text_item.setFont(font) 311 text_item.setDefaultTextColor(QtCore.Qt.white) 312 text_item.setFlags(QtWidgets.QGraphicsItem.ItemIgnoresTransformations) # QtWidgets.QGraphicsItem.ItemIsSelectable 313 314 text_item1 = QtWidgets.QGraphicsTextItem("text") 315 text_item1.setPos(initial_pos_p1) 316 font = text_item1.font() 317 font.setPointSize(10) 318 text_item1.setFont(font) 319 text_item1.setDefaultTextColor(QtCore.Qt.white) 320 text_item1.setFlags(QtWidgets.QGraphicsItem.ItemIgnoresTransformations) # QtWidgets.QGraphicsItem.ItemIsSelectable 321 322 text_item2 = QtWidgets.QGraphicsTextItem("text") 323 text_item2.setPos(initial_pos_p2) 324 font = text_item2.font() 325 font.setPointSize(10) 326 text_item2.setFont(font) 327 text_item2.setDefaultTextColor(QtCore.Qt.white) 328 text_item2.setFlags(QtWidgets.QGraphicsItem.ItemIgnoresTransformations) # QtWidgets.QGraphicsItem.ItemIsSelectable 329 330 width = 8 331 height = 8 332 333 point_topleft = QtCore.QPointF(-width/2, -height/2) 334 point_bottomright = QtCore.QPointF(width/2, height/2) 335 336 ellipse_rect = QtCore.QRectF(point_topleft, point_bottomright) 337 338 self.ellipse_item2 = CustomItem(ellipse_rect) 339 self.ellipse_item2.setPos(initial_pos_p2) 340 self.ellipse_item2.setBrush(brush_black) 341 self.ellipse_item2.setPen(pen) 342 343 line_item = QtWidgets.QGraphicsLineItem(QtCore.QLineF(40, 40, 80, 80)) 344 pen.setStyle(QtCore.Qt.SolidLine) 345 line_item.setPen(pen) 346 self.shadow_line = QtWidgets.QGraphicsDropShadowEffect(blurRadius=4, color=QtGui.QColor(0, 0, 0, 255), xOffset=0, yOffset=0) 347 348 self.ellipse_item1.set_relative_origin_position(self.relative_origin_position) 349 self.ellipse_item2.set_relative_origin_position(self.relative_origin_position) 350 351 self.ellipse_item1.add_line(line_item, True) 352 self.ellipse_item2.add_line(line_item, False) 353 self.ellipse_item1.add_text(text_item) 354 self.ellipse_item2.add_text(text_item) 355 self.ellipse_item1.add_text1(text_item1) 356 self.ellipse_item2.add_text1(text_item1) 357 self.ellipse_item1.add_text2(text_item2) 358 self.ellipse_item2.add_text2(text_item2) 359 360 self.ellipse_item1.set_px_per_unit(self.px_per_unit) 361 self.ellipse_item2.set_px_per_unit(self.px_per_unit) 362 self.ellipse_item1.set_unit(self.unit) 363 self.ellipse_item2.set_unit(self.unit) 364 365 line_item.setParentItem(self) 366 self.ellipse_item1.setParentItem(self) 367 self.ellipse_item2.setParentItem(self) 368 text_item.setParentItem(self) 369 text_item1.setParentItem(self) 370 text_item2.setParentItem(self) 371 372 self.ellipse_item2.refresh_positions() 373 self.ellipse_item1.refresh_positions() 374 375 def set_and_refresh_px_per_unit(self, px_per_unit): 376 """float: Set and refresh units conversion factor (for example, if the conversion is recalculated).""" 377 unit = self.unit 378 if "px" != unit: 379 mm_per_unit = self._mm_per_unit 380 self.px_per_unit = px_per_unit * mm_per_unit 381 self.ellipse_item1.set_px_per_unit(self.px_per_unit) 382 self.ellipse_item2.set_px_per_unit(self.px_per_unit) 383 self.ellipse_item2.refresh_positions() 384 self.ellipse_item1.refresh_positions() 385 386 def set_and_refresh_relative_origin_position(self, relative_origin_position): 387 """str: Set and refresh orientation of coordinate system (for example, if the orientation setting is changed).""" 388 self.relative_origin_position = relative_origin_position 389 self.ellipse_item1.set_relative_origin_position(self.relative_origin_position) 390 self.ellipse_item2.set_relative_origin_position(self.relative_origin_position) 391 self.ellipse_item2.refresh_positions() 392 self.ellipse_item1.refresh_positions() 393 394 395 396def main(): 397 app =QtWidgets.QApplication(sys.argv) 398 399 scene = QtWidgets.QGraphicsScene() 400 pixmap_item = QtWidgets.QGraphicsPixmapItem() 401 402 pixmap = QtGui.QPixmap(r"C:\image.png") 403 pixmap_item.setPixmap(pixmap) 404 405 scene.addItem(pixmap_item) 406 407 ruler = RulerItem() 408 409 scene.addItem(ruler) 410 411 ruler.setPos(50,100) 412 413 view = QtWidgets.QGraphicsView(scene) 414 view.show() 415 416 sys.exit(app.exec_()) 417 418 419 420if __name__ == '__main__': 421 main()
20class CustomItem(QtWidgets.QGraphicsEllipseItem): 21 """Create an endpoint for RulerItem and handle change in ruler position. 22 23 Instantiated with a QRectF as input argument. For example: 24 rect = QtCore.QRectF(point_topleft, point_bottomright) 25 endpoint = CustomItem(rect) 26 27 As implemented in RulerItem, the two endpoints reference each other when an endpoint is dragged. 28 This allows the other graphics items of the RulerItem to follow the endpoint movement. 29 """ 30 31 def __init__(self, *args, **kwargs): 32 super().__init__(*args, **kwargs) 33 34 self.setFlag(self.ItemIsMovable) 35 self.setFlag(self.ItemSendsGeometryChanges) 36 self.setFlag(self.ItemIgnoresTransformations) 37 self.line = None 38 self.is_point = None 39 self.text = None 40 self.text1 = None 41 self.text2 = None 42 self.unit = "px" 43 self.px_per_unit = 1.0 44 self.relative_origin_position = "bottomleft" 45 self.y_origin = -1 46 47 def add_line(self, line, is_point): 48 """Add QGraphicsLineItem as reference. 49 50 Args: 51 line (QGraphicsLineItem) 52 is_point (bool): True if endpoint is moving; False if static.""" 53 self.line = line 54 self.is_point = is_point 55 56 def add_text(self, text): 57 """QGraphicsTextItem: Add text item to center of ruler.""" 58 self.text = text 59 60 def add_text1(self, text): 61 """QGraphicsTextItem: Add text item to first endpoint of ruler.""" 62 self.text1 = text 63 64 def add_text2(self, text): 65 """QGraphicsTextItem: Add text item to second endpoint of ruler.""" 66 self.text2 = text 67 68 def set_px_per_unit(self, px_per_unit): 69 """float: Set conversion for pixels to specified unit of length.""" 70 self.px_per_unit = px_per_unit 71 72 def set_unit(self, unit): 73 """str: Set abbreviation for unit of length (for example, "mm" or "px").""" 74 self.unit = unit 75 76 def set_relative_origin_position(self, relative_origin_position): 77 """Set position of origin for coordinates, distances, and angles. 78 79 Two options for relative origin: 80 "topleft" has (0,0) at the top-left pixel of the image, which is typical for graphics 81 systems. One can think of this as a standard XY coordinate system mirrored about the 82 X-axis, where Y increases downwards. This means clockwise rotation is a positive angle. 83 84 "bottomright" has (0,0) at the bottom-left pixel of the image, just like a standard XY 85 coordinate system. This means counter-clockwise rotation is a positive angle. 86 87 Args: 88 relative_origin_position (str): "topleft" or "bottomleft" 89 """ 90 self.relative_origin_position = relative_origin_position 91 if self.relative_origin_position == "topleft": 92 self.y_origin = +1 93 elif self.relative_origin_position == "bottomleft": 94 self.y_origin = -1 95 96 def itemChange(self, change, value): 97 """Extend itemChange to update the positions and texts of the ruler line and labels.""" 98 if change == self.ItemPositionChange and self.scene(): 99 new_pos = value 100 101 self.move_line_to_center(new_pos) 102 103 self.update_text() 104 self.move_text(new_pos) 105 106 self.update_text1() 107 self.move_text1(new_pos) 108 109 self.update_text2() 110 self.move_text2(new_pos) 111 112 return super(CustomItem, self).itemChange(change, value) 113 114 def move_line_to_center(self, new_pos): 115 """QPointF: Set the center of the ruler line to a position.""" 116 x_offset = self.rect().x() + self.rect().width()/2 117 y_offset = self.rect().y() + self.rect().height()/2 118 119 new_center_pos = QtCore.QPointF(new_pos.x()+x_offset, new_pos.y()+y_offset) 120 121 p1 = new_center_pos if self.is_point else self.line.line().p1() 122 p2 = self.line.line().p2() if self.is_point else new_center_pos 123 124 self.line.setLine(QtCore.QLineF(p1, p2)) 125 126 def update_text(self): 127 """Refresh the text of the ruler's center label.""" 128 length_px = self.get_line_length(self.line.line()) 129 unit = self.unit 130 px_per_unit = self.px_per_unit 131 length_unit = length_px/px_per_unit 132 string_length = "{:.1f}".format(length_unit) + " " + unit 133 string = "<div style='background:rgba(0, 0, 0, 91);'>" + string_length + "</div>" 134 self.text.setHtml(string) 135 136 def update_text1(self): 137 """Refresh the text of the ruler's endpoint 1 label.""" 138 length_px = self.get_line_length(self.line.line()) 139 unit = self.unit 140 px_per_unit = self.px_per_unit 141 p1 = self.line.line().p1() 142 p2 = self.line.line().p2() 143 144 length_unit = length_px/px_per_unit 145 dx_unit = (p1.x()-p2.x())/px_per_unit 146 dy_unit = (p1.y()-p2.y())/px_per_unit 147 dy_unit *= self.y_origin 148 ang = math.degrees(math.atan2(dy_unit, dx_unit)) 149 150 string_abs = "|v| " + "{:.1f}".format(length_unit) + " " + unit 151 string_dx = "⬌ " + "{:.1f}".format(dx_unit) + " " + unit 152 string_dy = "⬍ " + "{:.1f}".format(dy_unit) + " " + unit 153 string_ang = "∠ " + "{:.1f}".format(ang) + "°" 154 string = "<div style='background:rgba(0, 0, 0, 91);'>" + string_abs + "<br>" + string_dx + "<br>" + string_dy + "<br>" + string_ang + "</div>" 155 self.text1.setHtml(string) 156 157 def update_text2(self): 158 """Refresh the text of the ruler's endpoint 2 label.""" 159 length_px = self.get_line_length(self.line.line()) 160 unit = self.unit 161 px_per_unit = self.px_per_unit 162 p1 = self.line.line().p1() 163 p2 = self.line.line().p2() 164 165 length_unit = length_px/px_per_unit 166 dx_unit = (p2.x()-p1.x())/px_per_unit 167 dy_unit = (p2.y()-p1.y())/px_per_unit 168 dy_unit *= self.y_origin 169 ang = math.degrees(math.atan2(dy_unit, dx_unit)) 170 171 string_abs = "|v| " + "{:.1f}".format(length_unit) + " " + unit 172 string_dx = "⬌ " + "{:.1f}".format(dx_unit) + " " + unit 173 string_dy = "⬍ " + "{:.1f}".format(dy_unit) + " " + unit 174 string_ang = "∠ " + "{:.1f}".format(ang) + "°" 175 string = "<div style='background:rgba(0, 0, 0, 91);'>" + string_abs + "<br>" + string_dx + "<br>" + string_dy + "<br>" + string_ang + "</div>" 176 self.text2.setHtml(string) 177 178 def move_text(self, new_pos): 179 """QPointF: Set the position of the ruler's center label.""" 180 if self.text: 181 center_pos = self.line.line().center() 182 x_offset = center_pos.x() 183 y_offset = center_pos.y() 184 new_pos = QtCore.QPointF(x_offset, y_offset) 185 self.text.setPos(new_pos) 186 187 def move_text1(self, new_pos): 188 """QPointF: Set the position of the ruler's endpoint 1 label.""" 189 if self.text: 190 pos = self.line.line().p1() 191 x_offset = pos.x() 192 y_offset = pos.y() 193 new_pos = QtCore.QPointF(x_offset, y_offset) 194 self.text1.setPos(new_pos) 195 196 def move_text2(self, new_pos): 197 """QPointF: Set the position of the ruler's endpoint 2 label.""" 198 if self.text: 199 pos = self.line.line().p2() 200 x_offset = pos.x() 201 y_offset = pos.y() 202 new_pos = QtCore.QPointF(x_offset, y_offset) 203 self.text2.setPos(new_pos) 204 205 def refresh_positions(self): 206 """Convenience function to refresh (update) positions of the ruler's line and endpoints.""" 207 self.move_line_to_center(self.pos()) 208 self.move_text(self.pos()) 209 self.update_text() 210 self.update_text1() 211 self.update_text2() 212 213 def get_line_length(self, line): 214 """Calculate the length of a QLineF. 215 216 Args: 217 line (QLineF) 218 219 Returns: 220 length (float) 221 """ 222 dx = line.x2() - line.x1() 223 dy = line.y2() - line.y1() 224 length = math.sqrt(dx**2 + dy**2) 225 return length
Create an endpoint for RulerItem and handle change in ruler position.
Instantiated with a QRectF as input argument. For example: rect = QtCore.QRectF(point_topleft, point_bottomright) endpoint = CustomItem(rect)
As implemented in RulerItem, the two endpoints reference each other when an endpoint is dragged. This allows the other graphics items of the RulerItem to follow the endpoint movement.
47 def add_line(self, line, is_point): 48 """Add QGraphicsLineItem as reference. 49 50 Args: 51 line (QGraphicsLineItem) 52 is_point (bool): True if endpoint is moving; False if static.""" 53 self.line = line 54 self.is_point = is_point
Add QGraphicsLineItem as reference.
Arguments:
- line (QGraphicsLineItem)
- is_point (bool): True if endpoint is moving; False if static.
56 def add_text(self, text): 57 """QGraphicsTextItem: Add text item to center of ruler.""" 58 self.text = text
QGraphicsTextItem: Add text item to center of ruler.
60 def add_text1(self, text): 61 """QGraphicsTextItem: Add text item to first endpoint of ruler.""" 62 self.text1 = text
QGraphicsTextItem: Add text item to first endpoint of ruler.
64 def add_text2(self, text): 65 """QGraphicsTextItem: Add text item to second endpoint of ruler.""" 66 self.text2 = text
QGraphicsTextItem: Add text item to second endpoint of ruler.
68 def set_px_per_unit(self, px_per_unit): 69 """float: Set conversion for pixels to specified unit of length.""" 70 self.px_per_unit = px_per_unit
float: Set conversion for pixels to specified unit of length.
72 def set_unit(self, unit): 73 """str: Set abbreviation for unit of length (for example, "mm" or "px").""" 74 self.unit = unit
str: Set abbreviation for unit of length (for example, "mm" or "px").
76 def set_relative_origin_position(self, relative_origin_position): 77 """Set position of origin for coordinates, distances, and angles. 78 79 Two options for relative origin: 80 "topleft" has (0,0) at the top-left pixel of the image, which is typical for graphics 81 systems. One can think of this as a standard XY coordinate system mirrored about the 82 X-axis, where Y increases downwards. This means clockwise rotation is a positive angle. 83 84 "bottomright" has (0,0) at the bottom-left pixel of the image, just like a standard XY 85 coordinate system. This means counter-clockwise rotation is a positive angle. 86 87 Args: 88 relative_origin_position (str): "topleft" or "bottomleft" 89 """ 90 self.relative_origin_position = relative_origin_position 91 if self.relative_origin_position == "topleft": 92 self.y_origin = +1 93 elif self.relative_origin_position == "bottomleft": 94 self.y_origin = -1
Set position of origin for coordinates, distances, and angles.
Two options for relative origin:
"topleft" has (0,0) at the top-left pixel of the image, which is typical for graphics systems. One can think of this as a standard XY coordinate system mirrored about the X-axis, where Y increases downwards. This means clockwise rotation is a positive angle.
"bottomright" has (0,0) at the bottom-left pixel of the image, just like a standard XY coordinate system. This means counter-clockwise rotation is a positive angle.
Arguments:
- relative_origin_position (str): "topleft" or "bottomleft"
96 def itemChange(self, change, value): 97 """Extend itemChange to update the positions and texts of the ruler line and labels.""" 98 if change == self.ItemPositionChange and self.scene(): 99 new_pos = value 100 101 self.move_line_to_center(new_pos) 102 103 self.update_text() 104 self.move_text(new_pos) 105 106 self.update_text1() 107 self.move_text1(new_pos) 108 109 self.update_text2() 110 self.move_text2(new_pos) 111 112 return super(CustomItem, self).itemChange(change, value)
Extend itemChange to update the positions and texts of the ruler line and labels.
114 def move_line_to_center(self, new_pos): 115 """QPointF: Set the center of the ruler line to a position.""" 116 x_offset = self.rect().x() + self.rect().width()/2 117 y_offset = self.rect().y() + self.rect().height()/2 118 119 new_center_pos = QtCore.QPointF(new_pos.x()+x_offset, new_pos.y()+y_offset) 120 121 p1 = new_center_pos if self.is_point else self.line.line().p1() 122 p2 = self.line.line().p2() if self.is_point else new_center_pos 123 124 self.line.setLine(QtCore.QLineF(p1, p2))
QPointF: Set the center of the ruler line to a position.
126 def update_text(self): 127 """Refresh the text of the ruler's center label.""" 128 length_px = self.get_line_length(self.line.line()) 129 unit = self.unit 130 px_per_unit = self.px_per_unit 131 length_unit = length_px/px_per_unit 132 string_length = "{:.1f}".format(length_unit) + " " + unit 133 string = "<div style='background:rgba(0, 0, 0, 91);'>" + string_length + "</div>" 134 self.text.setHtml(string)
Refresh the text of the ruler's center label.
136 def update_text1(self): 137 """Refresh the text of the ruler's endpoint 1 label.""" 138 length_px = self.get_line_length(self.line.line()) 139 unit = self.unit 140 px_per_unit = self.px_per_unit 141 p1 = self.line.line().p1() 142 p2 = self.line.line().p2() 143 144 length_unit = length_px/px_per_unit 145 dx_unit = (p1.x()-p2.x())/px_per_unit 146 dy_unit = (p1.y()-p2.y())/px_per_unit 147 dy_unit *= self.y_origin 148 ang = math.degrees(math.atan2(dy_unit, dx_unit)) 149 150 string_abs = "|v| " + "{:.1f}".format(length_unit) + " " + unit 151 string_dx = "⬌ " + "{:.1f}".format(dx_unit) + " " + unit 152 string_dy = "⬍ " + "{:.1f}".format(dy_unit) + " " + unit 153 string_ang = "∠ " + "{:.1f}".format(ang) + "°" 154 string = "<div style='background:rgba(0, 0, 0, 91);'>" + string_abs + "<br>" + string_dx + "<br>" + string_dy + "<br>" + string_ang + "</div>" 155 self.text1.setHtml(string)
Refresh the text of the ruler's endpoint 1 label.
157 def update_text2(self): 158 """Refresh the text of the ruler's endpoint 2 label.""" 159 length_px = self.get_line_length(self.line.line()) 160 unit = self.unit 161 px_per_unit = self.px_per_unit 162 p1 = self.line.line().p1() 163 p2 = self.line.line().p2() 164 165 length_unit = length_px/px_per_unit 166 dx_unit = (p2.x()-p1.x())/px_per_unit 167 dy_unit = (p2.y()-p1.y())/px_per_unit 168 dy_unit *= self.y_origin 169 ang = math.degrees(math.atan2(dy_unit, dx_unit)) 170 171 string_abs = "|v| " + "{:.1f}".format(length_unit) + " " + unit 172 string_dx = "⬌ " + "{:.1f}".format(dx_unit) + " " + unit 173 string_dy = "⬍ " + "{:.1f}".format(dy_unit) + " " + unit 174 string_ang = "∠ " + "{:.1f}".format(ang) + "°" 175 string = "<div style='background:rgba(0, 0, 0, 91);'>" + string_abs + "<br>" + string_dx + "<br>" + string_dy + "<br>" + string_ang + "</div>" 176 self.text2.setHtml(string)
Refresh the text of the ruler's endpoint 2 label.
178 def move_text(self, new_pos): 179 """QPointF: Set the position of the ruler's center label.""" 180 if self.text: 181 center_pos = self.line.line().center() 182 x_offset = center_pos.x() 183 y_offset = center_pos.y() 184 new_pos = QtCore.QPointF(x_offset, y_offset) 185 self.text.setPos(new_pos)
QPointF: Set the position of the ruler's center label.
187 def move_text1(self, new_pos): 188 """QPointF: Set the position of the ruler's endpoint 1 label.""" 189 if self.text: 190 pos = self.line.line().p1() 191 x_offset = pos.x() 192 y_offset = pos.y() 193 new_pos = QtCore.QPointF(x_offset, y_offset) 194 self.text1.setPos(new_pos)
QPointF: Set the position of the ruler's endpoint 1 label.
196 def move_text2(self, new_pos): 197 """QPointF: Set the position of the ruler's endpoint 2 label.""" 198 if self.text: 199 pos = self.line.line().p2() 200 x_offset = pos.x() 201 y_offset = pos.y() 202 new_pos = QtCore.QPointF(x_offset, y_offset) 203 self.text2.setPos(new_pos)
QPointF: Set the position of the ruler's endpoint 2 label.
205 def refresh_positions(self): 206 """Convenience function to refresh (update) positions of the ruler's line and endpoints.""" 207 self.move_line_to_center(self.pos()) 208 self.move_text(self.pos()) 209 self.update_text() 210 self.update_text1() 211 self.update_text2()
Convenience function to refresh (update) positions of the ruler's line and endpoints.
213 def get_line_length(self, line): 214 """Calculate the length of a QLineF. 215 216 Args: 217 line (QLineF) 218 219 Returns: 220 length (float) 221 """ 222 dx = line.x2() - line.x1() 223 dy = line.y2() - line.y1() 224 length = math.sqrt(dx**2 + dy**2) 225 return length
Calculate the length of a QLineF.
Arguments:
- line (QLineF)
Returns:
- length (float)
229class RulerItem(QtWidgets.QGraphicsRectItem): 230 """Create a movable ruler on QGraphicsScene with a specified unit of length. 231 232 Features: 233 Draggable endpoints. 234 Center label showing absolute length. 235 Endpoint labels showing difference in absolute length, horizontal and vertical delta, and angle. 236 237 Args: 238 unit (str): The text for labeling units of ruler values. 239 px_per_mm (float): The conversion for pixels to millimeters. For example, 10 means 10 240 pixels-per-mm, meaning the ruler value will show 1 mm when measuring 10 pixels. Set to 241 1.0 if the ruler has units of pixels. 242 initial_pos_p1 (QPointF): The position of endpoint 1 on the scene. 243 initial_pos_p2 (QPointF): The position of endpoint 2 on the scene. 244 relative_origin_position (str): The orientation of the origin for coordinate system 245 ("topleft" or "bottomleft"). 246 """ 247 248 def __init__(self, unit = "px", px_per_mm = None, initial_pos_p1=None, initial_pos_p2=None, relative_origin_position="bottomleft"): 249 super().__init__() 250 251 self.unit = unit 252 253 mm_per_unit = 1.0 254 if "cm" == unit: 255 mm_per_unit = 10.0 256 elif "m" == unit: 257 mm_per_unit = 1000.0 258 elif "in" == unit: 259 mm_per_unit = 25.4 260 elif "ft" == unit: 261 mm_per_unit = 304.8 262 elif "yd" == unit: 263 mm_per_unit = 914.4 264 265 if "px" == unit: 266 self.px_per_unit = 1.0 267 else: 268 self.px_per_unit = px_per_mm * mm_per_unit 269 270 self._mm_per_unit = mm_per_unit 271 272 self.relative_origin_position = relative_origin_position 273 274 if not initial_pos_p1: 275 initial_pos_p1 = QtCore.QPointF(10,10) 276 if not initial_pos_p2: 277 initial_pos_p2 = QtCore.QPointF(100,200) 278 279 pen = QtGui.QPen() 280 pen.setWidth(2) 281 pen.setCosmetic(True) 282 pen.setColor(QtCore.Qt.white) # setColor also works 283 pen.setCapStyle(QtCore.Qt.SquareCap) 284 pen.setJoinStyle(QtCore.Qt.MiterJoin) 285 286 brush = QtGui.QBrush() 287 brush.setColor(QtCore.Qt.white) 288 brush.setStyle(QtCore.Qt.SolidPattern) 289 290 brush_black = QtGui.QBrush() 291 brush_black.setColor(QtCore.Qt.black) 292 brush_black.setStyle(QtCore.Qt.SolidPattern) 293 294 295 width = 8 296 height = 8 297 point_topleft = QtCore.QPointF(-width/2, -height/2) 298 point_bottomright = QtCore.QPointF(width/2,height/2) 299 ellipse_rect = QtCore.QRectF(point_topleft, point_bottomright) 300 301 self.ellipse_item1 = CustomItem(ellipse_rect) 302 self.ellipse_item1.setPos(initial_pos_p1) 303 self.ellipse_item1.setBrush(brush_black) 304 self.ellipse_item1.setPen(pen) 305 306 307 text_item = QtWidgets.QGraphicsTextItem("text") 308 text_item.setPos(0,0) 309 font = text_item.font() 310 font.setPointSize(11) 311 text_item.setFont(font) 312 text_item.setDefaultTextColor(QtCore.Qt.white) 313 text_item.setFlags(QtWidgets.QGraphicsItem.ItemIgnoresTransformations) # QtWidgets.QGraphicsItem.ItemIsSelectable 314 315 text_item1 = QtWidgets.QGraphicsTextItem("text") 316 text_item1.setPos(initial_pos_p1) 317 font = text_item1.font() 318 font.setPointSize(10) 319 text_item1.setFont(font) 320 text_item1.setDefaultTextColor(QtCore.Qt.white) 321 text_item1.setFlags(QtWidgets.QGraphicsItem.ItemIgnoresTransformations) # QtWidgets.QGraphicsItem.ItemIsSelectable 322 323 text_item2 = QtWidgets.QGraphicsTextItem("text") 324 text_item2.setPos(initial_pos_p2) 325 font = text_item2.font() 326 font.setPointSize(10) 327 text_item2.setFont(font) 328 text_item2.setDefaultTextColor(QtCore.Qt.white) 329 text_item2.setFlags(QtWidgets.QGraphicsItem.ItemIgnoresTransformations) # QtWidgets.QGraphicsItem.ItemIsSelectable 330 331 width = 8 332 height = 8 333 334 point_topleft = QtCore.QPointF(-width/2, -height/2) 335 point_bottomright = QtCore.QPointF(width/2, height/2) 336 337 ellipse_rect = QtCore.QRectF(point_topleft, point_bottomright) 338 339 self.ellipse_item2 = CustomItem(ellipse_rect) 340 self.ellipse_item2.setPos(initial_pos_p2) 341 self.ellipse_item2.setBrush(brush_black) 342 self.ellipse_item2.setPen(pen) 343 344 line_item = QtWidgets.QGraphicsLineItem(QtCore.QLineF(40, 40, 80, 80)) 345 pen.setStyle(QtCore.Qt.SolidLine) 346 line_item.setPen(pen) 347 self.shadow_line = QtWidgets.QGraphicsDropShadowEffect(blurRadius=4, color=QtGui.QColor(0, 0, 0, 255), xOffset=0, yOffset=0) 348 349 self.ellipse_item1.set_relative_origin_position(self.relative_origin_position) 350 self.ellipse_item2.set_relative_origin_position(self.relative_origin_position) 351 352 self.ellipse_item1.add_line(line_item, True) 353 self.ellipse_item2.add_line(line_item, False) 354 self.ellipse_item1.add_text(text_item) 355 self.ellipse_item2.add_text(text_item) 356 self.ellipse_item1.add_text1(text_item1) 357 self.ellipse_item2.add_text1(text_item1) 358 self.ellipse_item1.add_text2(text_item2) 359 self.ellipse_item2.add_text2(text_item2) 360 361 self.ellipse_item1.set_px_per_unit(self.px_per_unit) 362 self.ellipse_item2.set_px_per_unit(self.px_per_unit) 363 self.ellipse_item1.set_unit(self.unit) 364 self.ellipse_item2.set_unit(self.unit) 365 366 line_item.setParentItem(self) 367 self.ellipse_item1.setParentItem(self) 368 self.ellipse_item2.setParentItem(self) 369 text_item.setParentItem(self) 370 text_item1.setParentItem(self) 371 text_item2.setParentItem(self) 372 373 self.ellipse_item2.refresh_positions() 374 self.ellipse_item1.refresh_positions() 375 376 def set_and_refresh_px_per_unit(self, px_per_unit): 377 """float: Set and refresh units conversion factor (for example, if the conversion is recalculated).""" 378 unit = self.unit 379 if "px" != unit: 380 mm_per_unit = self._mm_per_unit 381 self.px_per_unit = px_per_unit * mm_per_unit 382 self.ellipse_item1.set_px_per_unit(self.px_per_unit) 383 self.ellipse_item2.set_px_per_unit(self.px_per_unit) 384 self.ellipse_item2.refresh_positions() 385 self.ellipse_item1.refresh_positions() 386 387 def set_and_refresh_relative_origin_position(self, relative_origin_position): 388 """str: Set and refresh orientation of coordinate system (for example, if the orientation setting is changed).""" 389 self.relative_origin_position = relative_origin_position 390 self.ellipse_item1.set_relative_origin_position(self.relative_origin_position) 391 self.ellipse_item2.set_relative_origin_position(self.relative_origin_position) 392 self.ellipse_item2.refresh_positions() 393 self.ellipse_item1.refresh_positions()
Create a movable ruler on QGraphicsScene with a specified unit of length.
Features:
Draggable endpoints. Center label showing absolute length. Endpoint labels showing difference in absolute length, horizontal and vertical delta, and angle.
Arguments:
- unit (str): The text for labeling units of ruler values.
- px_per_mm (float): The conversion for pixels to millimeters. For example, 10 means 10 pixels-per-mm, meaning the ruler value will show 1 mm when measuring 10 pixels. Set to 1.0 if the ruler has units of pixels.
- initial_pos_p1 (QPointF): The position of endpoint 1 on the scene.
- initial_pos_p2 (QPointF): The position of endpoint 2 on the scene.
- relative_origin_position (str): The orientation of the origin for coordinate system ("topleft" or "bottomleft").
376 def set_and_refresh_px_per_unit(self, px_per_unit): 377 """float: Set and refresh units conversion factor (for example, if the conversion is recalculated).""" 378 unit = self.unit 379 if "px" != unit: 380 mm_per_unit = self._mm_per_unit 381 self.px_per_unit = px_per_unit * mm_per_unit 382 self.ellipse_item1.set_px_per_unit(self.px_per_unit) 383 self.ellipse_item2.set_px_per_unit(self.px_per_unit) 384 self.ellipse_item2.refresh_positions() 385 self.ellipse_item1.refresh_positions()
float: Set and refresh units conversion factor (for example, if the conversion is recalculated).
387 def set_and_refresh_relative_origin_position(self, relative_origin_position): 388 """str: Set and refresh orientation of coordinate system (for example, if the orientation setting is changed).""" 389 self.relative_origin_position = relative_origin_position 390 self.ellipse_item1.set_relative_origin_position(self.relative_origin_position) 391 self.ellipse_item2.set_relative_origin_position(self.relative_origin_position) 392 self.ellipse_item2.refresh_positions() 393 self.ellipse_item1.refresh_positions()
str: Set and refresh orientation of coordinate system (for example, if the orientation setting is changed).