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_unit (float): The conversion for pixels to units. For example, 10 means 10 239 pixels-per-unit, meaning the ruler value will show 1 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_unit = None, initial_pos_p1=None, initial_pos_p2=None, relative_origin_position="bottomleft"): 248 super().__init__() 249 250 self.unit = unit 251 self.px_per_unit = px_per_unit 252 self.relative_origin_position = relative_origin_position 253 254 if not initial_pos_p1: 255 initial_pos_p1 = QtCore.QPointF(10,10) 256 if not initial_pos_p2: 257 initial_pos_p2 = QtCore.QPointF(100,200) 258 259 pen = QtGui.QPen() 260 pen.setWidth(2) 261 pen.setCosmetic(True) 262 pen.setColor(QtCore.Qt.white) # setColor also works 263 pen.setCapStyle(QtCore.Qt.SquareCap) 264 pen.setJoinStyle(QtCore.Qt.MiterJoin) 265 266 brush = QtGui.QBrush() 267 brush.setColor(QtCore.Qt.white) 268 brush.setStyle(QtCore.Qt.SolidPattern) 269 270 brush_black = QtGui.QBrush() 271 brush_black.setColor(QtCore.Qt.black) 272 brush_black.setStyle(QtCore.Qt.SolidPattern) 273 274 275 width = 8 276 height = 8 277 point_topleft = QtCore.QPointF(-width/2, -height/2) 278 point_bottomright = QtCore.QPointF(width/2,height/2) 279 ellipse_rect = QtCore.QRectF(point_topleft, point_bottomright) 280 281 self.ellipse_item1 = CustomItem(ellipse_rect) 282 self.ellipse_item1.setPos(initial_pos_p1) 283 self.ellipse_item1.setBrush(brush_black) 284 self.ellipse_item1.setPen(pen) 285 286 287 text_item = QtWidgets.QGraphicsTextItem("text") 288 text_item.setPos(0,0) 289 font = text_item.font() 290 font.setPointSize(11) 291 text_item.setFont(font) 292 text_item.setDefaultTextColor(QtCore.Qt.white) 293 text_item.setFlags(QtWidgets.QGraphicsItem.ItemIgnoresTransformations) # QtWidgets.QGraphicsItem.ItemIsSelectable 294 295 text_item1 = QtWidgets.QGraphicsTextItem("text") 296 text_item1.setPos(initial_pos_p1) 297 font = text_item1.font() 298 font.setPointSize(10) 299 text_item1.setFont(font) 300 text_item1.setDefaultTextColor(QtCore.Qt.white) 301 text_item1.setFlags(QtWidgets.QGraphicsItem.ItemIgnoresTransformations) # QtWidgets.QGraphicsItem.ItemIsSelectable 302 303 text_item2 = QtWidgets.QGraphicsTextItem("text") 304 text_item2.setPos(initial_pos_p2) 305 font = text_item2.font() 306 font.setPointSize(10) 307 text_item2.setFont(font) 308 text_item2.setDefaultTextColor(QtCore.Qt.white) 309 text_item2.setFlags(QtWidgets.QGraphicsItem.ItemIgnoresTransformations) # QtWidgets.QGraphicsItem.ItemIsSelectable 310 311 width = 8 312 height = 8 313 314 point_topleft = QtCore.QPointF(-width/2, -height/2) 315 point_bottomright = QtCore.QPointF(width/2, height/2) 316 317 ellipse_rect = QtCore.QRectF(point_topleft, point_bottomright) 318 319 self.ellipse_item2 = CustomItem(ellipse_rect) 320 self.ellipse_item2.setPos(initial_pos_p2) 321 self.ellipse_item2.setBrush(brush_black) 322 self.ellipse_item2.setPen(pen) 323 324 line_item = QtWidgets.QGraphicsLineItem(QtCore.QLineF(40, 40, 80, 80)) 325 pen.setStyle(QtCore.Qt.SolidLine) 326 line_item.setPen(pen) 327 self.shadow_line = QtWidgets.QGraphicsDropShadowEffect(blurRadius=4, color=QtGui.QColor(0, 0, 0, 255), xOffset=0, yOffset=0) 328 329 self.ellipse_item1.set_relative_origin_position(self.relative_origin_position) 330 self.ellipse_item2.set_relative_origin_position(self.relative_origin_position) 331 332 self.ellipse_item1.add_line(line_item, True) 333 self.ellipse_item2.add_line(line_item, False) 334 self.ellipse_item1.add_text(text_item) 335 self.ellipse_item2.add_text(text_item) 336 self.ellipse_item1.add_text1(text_item1) 337 self.ellipse_item2.add_text1(text_item1) 338 self.ellipse_item1.add_text2(text_item2) 339 self.ellipse_item2.add_text2(text_item2) 340 341 self.ellipse_item1.set_px_per_unit(self.px_per_unit) 342 self.ellipse_item2.set_px_per_unit(self.px_per_unit) 343 self.ellipse_item1.set_unit(self.unit) 344 self.ellipse_item2.set_unit(self.unit) 345 346 line_item.setParentItem(self) 347 self.ellipse_item1.setParentItem(self) 348 self.ellipse_item2.setParentItem(self) 349 text_item.setParentItem(self) 350 text_item1.setParentItem(self) 351 text_item2.setParentItem(self) 352 353 self.ellipse_item2.refresh_positions() 354 self.ellipse_item1.refresh_positions() 355 356 def set_and_refresh_px_per_unit(self, px_per_unit): 357 """float: Set and refresh units conversion factor (for example, if the conversion is recalculated).""" 358 self.px_per_unit = px_per_unit 359 self.ellipse_item1.set_px_per_unit(self.px_per_unit) 360 self.ellipse_item2.set_px_per_unit(self.px_per_unit) 361 self.ellipse_item2.refresh_positions() 362 self.ellipse_item1.refresh_positions() 363 364 def set_and_refresh_relative_origin_position(self, relative_origin_position): 365 """str: Set and refresh orientation of coordinate system (for example, if the orientation setting is changed).""" 366 self.relative_origin_position = relative_origin_position 367 self.ellipse_item1.set_relative_origin_position(self.relative_origin_position) 368 self.ellipse_item2.set_relative_origin_position(self.relative_origin_position) 369 self.ellipse_item2.refresh_positions() 370 self.ellipse_item1.refresh_positions() 371 372 373 374def main(): 375 app =QtWidgets.QApplication(sys.argv) 376 377 scene = QtWidgets.QGraphicsScene() 378 pixmap_item = QtWidgets.QGraphicsPixmapItem() 379 380 pixmap = QtGui.QPixmap(r"C:\image.png") 381 pixmap_item.setPixmap(pixmap) 382 383 scene.addItem(pixmap_item) 384 385 ruler = RulerItem() 386 387 scene.addItem(ruler) 388 389 ruler.setPos(50,100) 390 391 view = QtWidgets.QGraphicsView(scene) 392 view.show() 393 394 sys.exit(app.exec_()) 395 396 397 398if __name__ == '__main__': 399 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_unit (float): The conversion for pixels to units. For example, 10 means 10 240 pixels-per-unit, meaning the ruler value will show 1 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_unit = None, initial_pos_p1=None, initial_pos_p2=None, relative_origin_position="bottomleft"): 249 super().__init__() 250 251 self.unit = unit 252 self.px_per_unit = px_per_unit 253 self.relative_origin_position = relative_origin_position 254 255 if not initial_pos_p1: 256 initial_pos_p1 = QtCore.QPointF(10,10) 257 if not initial_pos_p2: 258 initial_pos_p2 = QtCore.QPointF(100,200) 259 260 pen = QtGui.QPen() 261 pen.setWidth(2) 262 pen.setCosmetic(True) 263 pen.setColor(QtCore.Qt.white) # setColor also works 264 pen.setCapStyle(QtCore.Qt.SquareCap) 265 pen.setJoinStyle(QtCore.Qt.MiterJoin) 266 267 brush = QtGui.QBrush() 268 brush.setColor(QtCore.Qt.white) 269 brush.setStyle(QtCore.Qt.SolidPattern) 270 271 brush_black = QtGui.QBrush() 272 brush_black.setColor(QtCore.Qt.black) 273 brush_black.setStyle(QtCore.Qt.SolidPattern) 274 275 276 width = 8 277 height = 8 278 point_topleft = QtCore.QPointF(-width/2, -height/2) 279 point_bottomright = QtCore.QPointF(width/2,height/2) 280 ellipse_rect = QtCore.QRectF(point_topleft, point_bottomright) 281 282 self.ellipse_item1 = CustomItem(ellipse_rect) 283 self.ellipse_item1.setPos(initial_pos_p1) 284 self.ellipse_item1.setBrush(brush_black) 285 self.ellipse_item1.setPen(pen) 286 287 288 text_item = QtWidgets.QGraphicsTextItem("text") 289 text_item.setPos(0,0) 290 font = text_item.font() 291 font.setPointSize(11) 292 text_item.setFont(font) 293 text_item.setDefaultTextColor(QtCore.Qt.white) 294 text_item.setFlags(QtWidgets.QGraphicsItem.ItemIgnoresTransformations) # QtWidgets.QGraphicsItem.ItemIsSelectable 295 296 text_item1 = QtWidgets.QGraphicsTextItem("text") 297 text_item1.setPos(initial_pos_p1) 298 font = text_item1.font() 299 font.setPointSize(10) 300 text_item1.setFont(font) 301 text_item1.setDefaultTextColor(QtCore.Qt.white) 302 text_item1.setFlags(QtWidgets.QGraphicsItem.ItemIgnoresTransformations) # QtWidgets.QGraphicsItem.ItemIsSelectable 303 304 text_item2 = QtWidgets.QGraphicsTextItem("text") 305 text_item2.setPos(initial_pos_p2) 306 font = text_item2.font() 307 font.setPointSize(10) 308 text_item2.setFont(font) 309 text_item2.setDefaultTextColor(QtCore.Qt.white) 310 text_item2.setFlags(QtWidgets.QGraphicsItem.ItemIgnoresTransformations) # QtWidgets.QGraphicsItem.ItemIsSelectable 311 312 width = 8 313 height = 8 314 315 point_topleft = QtCore.QPointF(-width/2, -height/2) 316 point_bottomright = QtCore.QPointF(width/2, height/2) 317 318 ellipse_rect = QtCore.QRectF(point_topleft, point_bottomright) 319 320 self.ellipse_item2 = CustomItem(ellipse_rect) 321 self.ellipse_item2.setPos(initial_pos_p2) 322 self.ellipse_item2.setBrush(brush_black) 323 self.ellipse_item2.setPen(pen) 324 325 line_item = QtWidgets.QGraphicsLineItem(QtCore.QLineF(40, 40, 80, 80)) 326 pen.setStyle(QtCore.Qt.SolidLine) 327 line_item.setPen(pen) 328 self.shadow_line = QtWidgets.QGraphicsDropShadowEffect(blurRadius=4, color=QtGui.QColor(0, 0, 0, 255), xOffset=0, yOffset=0) 329 330 self.ellipse_item1.set_relative_origin_position(self.relative_origin_position) 331 self.ellipse_item2.set_relative_origin_position(self.relative_origin_position) 332 333 self.ellipse_item1.add_line(line_item, True) 334 self.ellipse_item2.add_line(line_item, False) 335 self.ellipse_item1.add_text(text_item) 336 self.ellipse_item2.add_text(text_item) 337 self.ellipse_item1.add_text1(text_item1) 338 self.ellipse_item2.add_text1(text_item1) 339 self.ellipse_item1.add_text2(text_item2) 340 self.ellipse_item2.add_text2(text_item2) 341 342 self.ellipse_item1.set_px_per_unit(self.px_per_unit) 343 self.ellipse_item2.set_px_per_unit(self.px_per_unit) 344 self.ellipse_item1.set_unit(self.unit) 345 self.ellipse_item2.set_unit(self.unit) 346 347 line_item.setParentItem(self) 348 self.ellipse_item1.setParentItem(self) 349 self.ellipse_item2.setParentItem(self) 350 text_item.setParentItem(self) 351 text_item1.setParentItem(self) 352 text_item2.setParentItem(self) 353 354 self.ellipse_item2.refresh_positions() 355 self.ellipse_item1.refresh_positions() 356 357 def set_and_refresh_px_per_unit(self, px_per_unit): 358 """float: Set and refresh units conversion factor (for example, if the conversion is recalculated).""" 359 self.px_per_unit = px_per_unit 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_item2.refresh_positions() 363 self.ellipse_item1.refresh_positions() 364 365 def set_and_refresh_relative_origin_position(self, relative_origin_position): 366 """str: Set and refresh orientation of coordinate system (for example, if the orientation setting is changed).""" 367 self.relative_origin_position = relative_origin_position 368 self.ellipse_item1.set_relative_origin_position(self.relative_origin_position) 369 self.ellipse_item2.set_relative_origin_position(self.relative_origin_position) 370 self.ellipse_item2.refresh_positions() 371 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_unit (float): The conversion for pixels to units. For example, 10 means 10 pixels-per-unit, meaning the ruler value will show 1 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").
357 def set_and_refresh_px_per_unit(self, px_per_unit): 358 """float: Set and refresh units conversion factor (for example, if the conversion is recalculated).""" 359 self.px_per_unit = px_per_unit 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_item2.refresh_positions() 363 self.ellipse_item1.refresh_positions()
float: Set and refresh units conversion factor (for example, if the conversion is recalculated).
365 def set_and_refresh_relative_origin_position(self, relative_origin_position): 366 """str: Set and refresh orientation of coordinate system (for example, if the orientation setting is changed).""" 367 self.relative_origin_position = relative_origin_position 368 self.ellipse_item1.set_relative_origin_position(self.relative_origin_position) 369 self.ellipse_item2.set_relative_origin_position(self.relative_origin_position) 370 self.ellipse_item2.refresh_positions() 371 self.ellipse_item1.refresh_positions()
str: Set and refresh orientation of coordinate system (for example, if the orientation setting is changed).