hytos / DTI_PID / DTI_PID / QtImageViewer.py @ f5fc3a88
이력 | 보기 | 이력해설 | 다운로드 (41.5 KB)
1 |
# coding: utf-8
|
---|---|
2 |
import sys |
3 |
import os.path |
4 |
|
5 |
try:
|
6 |
from PyQt5.QtCore import * |
7 |
from PyQt5.QtGui import * |
8 |
from PyQt5.QtWidgets import * |
9 |
except ImportError: |
10 |
try:
|
11 |
from PyQt4.QtCore import * |
12 |
from PyQt4.QtGui import * |
13 |
except ImportError: |
14 |
raise ImportError("ImageViewerQt: Requires PyQt5 or PyQt4.") |
15 |
|
16 |
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)) + '\\Commands') |
17 |
import DefaultCommand |
18 |
|
19 |
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)) + '\\Shapes') |
20 |
from EngineeringLineItem import QEngineeringLineItem |
21 |
from EngineeringTextItem import QEngineeringTextItem |
22 |
from EngineeringSpecBreakItem import QEngineeringSpecBreakItem |
23 |
from SymbolSvgItem import SymbolSvgItem |
24 |
|
25 |
__author__ = "Marcel Goldschen-Ohm <marcel.goldschen@gmail.com>"
|
26 |
__version__ = '0.9.0'
|
27 |
|
28 |
|
29 |
class QtImageViewer(QGraphicsView): |
30 |
""" PyQt image viewer widget for a QPixmap in a QGraphicsView scene with mouse zooming and panning.
|
31 |
Displays a QImage or QPixmap (QImage is internally converted to a QPixmap).
|
32 |
To display any other image format, you must first convert it to a QImage or QPixmap.
|
33 |
Some useful image format conversion utilities:
|
34 |
qimage2ndarray: NumPy ndarray <==> QImage (https://github.com/hmeine/qimage2ndarray)
|
35 |
ImageQt: PIL Image <==> QImage (https://github.com/python-pillow/Pillow/blob/master/PIL/ImageQt.py)
|
36 |
Mouse interaction:
|
37 |
Left mouse button drag: Pan image.
|
38 |
Right mouse button drag: Zoom box.
|
39 |
Right mouse button doubleclick: Zoom to show entire image.
|
40 |
"""
|
41 |
|
42 |
# Mouse button signals emit image scene (x, y) coordinates.
|
43 |
# !!! For image (row, column) matrix indexing, row = y and column = x.
|
44 |
leftMouseButtonPressed = pyqtSignal(float, float) |
45 |
rightMouseButtonPressed = pyqtSignal(float, float) |
46 |
leftMouseButtonMoved = pyqtSignal(float, float) |
47 |
rightMouseButtonMoved = pyqtSignal(float, float) |
48 |
leftMouseButtonReleased = pyqtSignal(float, float) |
49 |
rightMouseButtonReleased = pyqtSignal(float, float) |
50 |
leftMouseButtonDoubleClicked = pyqtSignal(float, float) |
51 |
rightMouseButtonDoubleClicked = pyqtSignal(float, float) |
52 |
# itemRemoved = pyqtSignal(QGraphicsItem)
|
53 |
startPointChanged = pyqtSignal(float, float) |
54 |
|
55 |
'''
|
56 |
@history 2018.06.27 Jeongwoo Change zoom rule (Qt.KeepAspectRatioByExpanding → Qt.KeepAspectRatio)
|
57 |
'''
|
58 |
|
59 |
def __init__(self, mainWindow=None): |
60 |
QGraphicsView.__init__(self)
|
61 |
|
62 |
self.mainWindow = mainWindow
|
63 |
# Image is displayed as a QPixmap in a QGraphicsScene attached to this QGraphicsView.
|
64 |
self.command = None |
65 |
|
66 |
self.scaleFactor = 1.0 |
67 |
self.numScheduledScalings = 0 |
68 |
self.isOriginalPointSelected = False |
69 |
|
70 |
# Store a local handle to the scene's current image pixmap.
|
71 |
self._pixmapHandle = None |
72 |
|
73 |
# Image aspect ratio mode.
|
74 |
# !!! ONLY applies to full image. Aspect ratio is always ignored when zooming.
|
75 |
# Qt.IgnoreAspectRatio: Scale image to fit viewport.
|
76 |
# Qt.KeepAspectRatio: Scale image to fit inside viewport, preserving aspect ratio.
|
77 |
# Qt.KeepAspectRatioByExpanding: Scale image to fill the viewport, preserving aspect ratio.
|
78 |
self.aspectRatioMode = Qt.KeepAspectRatio
|
79 |
|
80 |
# Scroll bar behaviour.
|
81 |
# Qt.ScrollBarAlwaysOff: Never shows a scroll bar.
|
82 |
# Qt.ScrollBarAlwaysOn: Always shows a scroll bar.
|
83 |
# Qt.ScrollBarAsNeeded: Shows a scroll bar only when zoomed.
|
84 |
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
85 |
self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
86 |
# self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
87 |
# self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
88 |
|
89 |
# Stack of QRectF zoom boxes in scene coordinates.
|
90 |
self.zoomStack = []
|
91 |
|
92 |
self.setRenderHint(QPainter.Antialiasing)
|
93 |
|
94 |
self.setAcceptDrops(True) # enable drop |
95 |
|
96 |
# Flags for enabling/disabling mouse interaction.
|
97 |
self.canZoom = True |
98 |
self.canPan = True |
99 |
self.setMouseTracking(True) |
100 |
self.command = None |
101 |
|
102 |
# set currentAttribute
|
103 |
self.currentAttribute = '' |
104 |
|
105 |
# image stack for symboleditor
|
106 |
self._image_stack = []
|
107 |
self._image_stack_index = -1 |
108 |
|
109 |
'''
|
110 |
@brief Return Pixmap Handler
|
111 |
@author Jeongwoo
|
112 |
@date 2018.06.11
|
113 |
'''
|
114 |
|
115 |
def getPixmapHandle(self): |
116 |
return self._pixmapHandle |
117 |
|
118 |
'''
|
119 |
@brief Use Default ImageViewer Command
|
120 |
@author Jeongwoo
|
121 |
@date 18.04.10
|
122 |
@history .
|
123 |
'''
|
124 |
|
125 |
def useDefaultCommand(self): |
126 |
""" Use Default Command
|
127 |
"""
|
128 |
self.command = DefaultCommand.DefaultCommand(self) |
129 |
|
130 |
def hasImage(self): |
131 |
""" Returns whether or not the scene contains an image pixmap.
|
132 |
"""
|
133 |
return self._pixmapHandle is not None |
134 |
|
135 |
def clearImage(self): |
136 |
""" Removes the current image pixmap from the scene if it exists.
|
137 |
"""
|
138 |
if self.hasImage(): |
139 |
self.scene().removeItem(self._pixmapHandle) |
140 |
self._pixmapHandle = None |
141 |
|
142 |
def pixmap(self): |
143 |
""" Returns the scene's current image pixmap as a QPixmap, or else None if no image exists.
|
144 |
:rtype: QPixmap | None
|
145 |
"""
|
146 |
if self.hasImage(): |
147 |
return self._pixmapHandle.pixmap() |
148 |
return None |
149 |
|
150 |
def image(self): |
151 |
""" Returns the scene's current image pixmap as a QImage, or else None if no image exists.
|
152 |
:rtype: QImage | None
|
153 |
"""
|
154 |
if self.hasImage(): |
155 |
return self._pixmapHandle.pixmap().toImage() |
156 |
return None |
157 |
|
158 |
def setImage(self, image, clear_object=True): |
159 |
""" Set the scene's current image pixmap to the input QImage or QPixmap.
|
160 |
Raises a RuntimeError if the input image has type other than QImage or QPixmap.
|
161 |
:type image: QImage | QPixmap
|
162 |
"""
|
163 |
try:
|
164 |
if type(image) is QPixmap: |
165 |
pixmap = image |
166 |
elif type(image) is QImage: |
167 |
pixmap = QPixmap.fromImage(image) |
168 |
else:
|
169 |
raise RuntimeError("ImageViewer.setImage: Argument must be a QImage or QPixmap.") |
170 |
|
171 |
self.clearImage()
|
172 |
if clear_object:
|
173 |
self.scene().clear()
|
174 |
|
175 |
if self.hasImage(): |
176 |
self._pixmapHandle.setPixmap(pixmap)
|
177 |
else:
|
178 |
self._pixmapHandle = self.scene().addPixmap(pixmap) |
179 |
self._pixmapHandle.setFlags(QGraphicsItem.ItemClipsChildrenToShape)
|
180 |
self._pixmapHandle.setZValue(-1) |
181 |
|
182 |
self.setSceneRect(QRectF(pixmap.rect())) # Set scene size to image size. |
183 |
self.updateViewer()
|
184 |
except Exception as ex: |
185 |
print('error occurred({}) in {}:{}'.format(ex, sys.exc_info()[-1].tb_frame.f_code.co_filename, |
186 |
sys.exc_info()[-1].tb_lineno))
|
187 |
|
188 |
'''
|
189 |
@brief open a image file selected by user
|
190 |
@author
|
191 |
@date
|
192 |
'''
|
193 |
def loadImageFromFile(self, drawing): |
194 |
import cv2 |
195 |
import numpy as np |
196 |
from AppDocData import AppDocData |
197 |
""" Load an image from file.
|
198 |
Without any arguments, loadImageFromFile() will popup a file dialog to choose the image file.
|
199 |
With a fileName argument, loadImageFromFile(fileName) will attempt to load the specified image file directly.
|
200 |
"""
|
201 |
|
202 |
file_path = None
|
203 |
try:
|
204 |
app_doc_data = AppDocData.instance() |
205 |
|
206 |
cvImg = None
|
207 |
if drawing:
|
208 |
file_path = drawing.file_path |
209 |
cvImg = drawing.image |
210 |
else:
|
211 |
options = QFileDialog.Options() |
212 |
options |= QFileDialog.DontUseNativeDialog |
213 |
if QT_VERSION_STR[0] == '4': |
214 |
file_path = QFileDialog.getOpenFileName(self, "Open image file", |
215 |
app_doc_data.project.getDrawingFilePath(), |
216 |
"Image files(*.png *.jpg)", options=options)
|
217 |
elif QT_VERSION_STR[0] == '5': |
218 |
file_path, _ = QFileDialog.getOpenFileName(self, "Open image file", |
219 |
app_doc_data.project.getDrawingFilePath(), |
220 |
"Image files(*.png *.jpg)", options=options)
|
221 |
|
222 |
_bytes = None
|
223 |
with open(file_path.encode('utf-8'), 'rb') as stream: |
224 |
_bytes = stream.read() |
225 |
|
226 |
numpyArray = np.asarray(bytearray(_bytes), dtype=np.uint8)
|
227 |
image = cv2.imdecode(numpyArray, cv2.IMREAD_UNCHANGED) |
228 |
cvImg = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) |
229 |
cvImg = cv2.threshold(cvImg, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1] |
230 |
|
231 |
configs = app_doc_data.getConfigs('Filter', 'DilateSize') |
232 |
if 1 == len(configs) and int(configs[0].value) is not 0: |
233 |
size = int(configs[0].value) |
234 |
kernel = np.ones((size, size), np.uint8) |
235 |
cvImg = cv2.erode(cvImg, kernel, iterations=1)
|
236 |
|
237 |
configs = app_doc_data.getConfigs('Filter', 'FlatSize') |
238 |
if 1 == len(configs) and int(configs[0].value) is not 0: |
239 |
size = int(configs[0].value) |
240 |
kernel = np.ones((size, size), np.uint8) |
241 |
cvImg = cv2.morphologyEx(cvImg, cv2.MORPH_CLOSE, kernel) |
242 |
cvImg = cv2.morphologyEx(cvImg, cv2.MORPH_OPEN, kernel) |
243 |
|
244 |
bytesPerLine = cvImg.shape[1]
|
245 |
image = QImage(cvImg.data, cvImg.shape[1], cvImg.shape[0], bytesPerLine, QImage.Format_Indexed8) |
246 |
self.setImage(image)
|
247 |
except Exception as ex: |
248 |
from App import App |
249 |
from AppDocData import MessageType |
250 |
|
251 |
message = 'error occurred({}) in {}:{}'.format(repr(ex), sys.exc_info()[-1].tb_frame.f_code.co_filename, |
252 |
sys.exc_info()[-1].tb_lineno)
|
253 |
App.mainWnd().addMessage.emit(MessageType.Error, message) |
254 |
|
255 |
return file_path
|
256 |
|
257 |
def zoom_rect(self, rect: QRectF) -> None: |
258 |
"""zoom window within given rectangle"""
|
259 |
self.fitInView(rect, Qt.KeepAspectRatio)
|
260 |
if not self.zoomStack: |
261 |
self.zoomStack = []
|
262 |
|
263 |
self.zoomStack.append(rect)
|
264 |
|
265 |
|
266 |
'''
|
267 |
@history 2018.06.27 Jeongwoo Change zoom rule (Qt.KeepAspectRatioByExpanding → Qt.KeepAspectRatio)
|
268 |
'''
|
269 |
|
270 |
def updateViewer(self, zoomNewRect=None): |
271 |
"""Show current zoom (if showing entire image, apply current aspect ratio mode)."""
|
272 |
if not self.hasImage(): |
273 |
return
|
274 |
|
275 |
if zoomNewRect is not None: |
276 |
self.fitInView(zoomNewRect, Qt.KeepAspectRatio)
|
277 |
else:
|
278 |
if self.zoomStack: |
279 |
if zoomNewRect is None: |
280 |
self.fitInView(self.zoomStack[-1], Qt.KeepAspectRatio) # Show zoomed rect (ignore aspect ratio). |
281 |
else:
|
282 |
self.zoomStack = [] # Clear the zoom stack (in case we got here because of an invalid zoom). |
283 |
self.fitInView(self.sceneRect(), |
284 |
self.aspectRatioMode) # Show entire image (use current aspect ratio mode). |
285 |
|
286 |
def zoomImageInit(self): |
287 |
if self.hasImage(): |
288 |
self.zoomStack = []
|
289 |
self.updateViewer()
|
290 |
self.setCursor(QCursor(Qt.ArrowCursor))
|
291 |
|
292 |
'''
|
293 |
@brief Zoom in & out image
|
294 |
@author Jeongwoo
|
295 |
@date -
|
296 |
@history 18.04.11 Jeongwoo add parameter 'adjust' (@ref ResultTreeWidget.itemClickEvent(self, item, columnNo))
|
297 |
'''
|
298 |
def zoomImage(self, isZoomIn, event, adjust=1): |
299 |
"""Zoom in & out """
|
300 |
|
301 |
HALF_SIZE = 300
|
302 |
clickPos = event.pos() |
303 |
if isZoomIn:
|
304 |
left_top = self.mapToScene(clickPos.x() - HALF_SIZE // adjust, clickPos.y() - HALF_SIZE // adjust)
|
305 |
right_bottom = self.mapToScene(clickPos.x() + HALF_SIZE // adjust, clickPos.y() + HALF_SIZE // adjust)
|
306 |
|
307 |
zoomArea = QRectF(left_top, right_bottom) |
308 |
self.scene().setSelectionArea(QPainterPath()) # Clear current selection area. |
309 |
if zoomArea.isValid():
|
310 |
self.zoomStack.append(zoomArea)
|
311 |
self.updateViewer(zoomArea)
|
312 |
else:
|
313 |
zoomNewRect = None
|
314 |
self.scene().clearSelection()
|
315 |
if self.zoomStack: |
316 |
clickPos = self.mapToScene(clickPos.x(), clickPos.y())
|
317 |
zoomNewRect = self.zoomStack.pop()
|
318 |
zoomNewRect = QRectF(clickPos.x() - zoomNewRect.width() / 2, clickPos.y() - zoomNewRect.height() / 2, zoomNewRect.width(), zoomNewRect.height()) |
319 |
else:
|
320 |
left_top = self.mapToScene(clickPos.x() - HALF_SIZE // adjust, clickPos.y() - HALF_SIZE // adjust)
|
321 |
right_bottom = self.mapToScene(clickPos.x() + HALF_SIZE // adjust, clickPos.y() + HALF_SIZE // adjust)
|
322 |
|
323 |
zoomArea = QRectF(left_top, right_bottom) |
324 |
self.scene().clearSelection()
|
325 |
if zoomArea.isValid():
|
326 |
zoomNewRect = zoomArea |
327 |
|
328 |
self.updateViewer(zoomNewRect)
|
329 |
|
330 |
'''
|
331 |
@brief mouse move event
|
332 |
'''
|
333 |
def mouseMoveEvent(self, event): |
334 |
try:
|
335 |
scenePos = self.mapToScene(event.pos())
|
336 |
if self.command is not None: |
337 |
self.command.execute(['mouseMoveEvent', event, scenePos]) |
338 |
if self.command.name == "SelectAttribute": |
339 |
QGraphicsView.mouseMoveEvent(self, event)
|
340 |
if self.command.isTreated: |
341 |
event.accept() |
342 |
return
|
343 |
except Exception as ex: |
344 |
print('error occurred({}) in {}:{}'.format(ex, sys.exc_info()[-1].tb_frame.f_code.co_filename, |
345 |
sys.exc_info()[-1].tb_lineno))
|
346 |
|
347 |
if self.scene().guidesEnabled: |
348 |
self.scene().coords = self.mapToScene(event.pos()) |
349 |
self.scene().invalidate()
|
350 |
|
351 |
QGraphicsView.mouseMoveEvent(self, event)
|
352 |
|
353 |
'''
|
354 |
@brief
|
355 |
@author
|
356 |
@date
|
357 |
@history block clear selection when right mouse button is clicked
|
358 |
'''
|
359 |
def mousePressEvent(self, event): |
360 |
try:
|
361 |
if self.command is not None: |
362 |
# for symboleditor
|
363 |
if self.window().objectName() == 'SymbolEditorDialog': |
364 |
self._image_stack_index = self._image_stack_index + 1 |
365 |
self._image_stack.insert(self._image_stack_index, self.image().copy()) |
366 |
|
367 |
scenePos = self.mapToScene(event.pos())
|
368 |
self.command.execute(['mousePressEvent', event, scenePos]) |
369 |
if self.command.isTreated: |
370 |
event.accept() |
371 |
return
|
372 |
except Exception as ex: |
373 |
from App import App |
374 |
from AppDocData import MessageType |
375 |
|
376 |
message = 'error occurred({}) in {}:{}'.format(repr(ex), sys.exc_info()[-1].tb_frame.f_code.co_filename, |
377 |
sys.exc_info()[-1].tb_lineno)
|
378 |
App.mainWnd().addMessage.emit(MessageType.Error, message) |
379 |
|
380 |
if event.button() != Qt.RightButton:
|
381 |
QGraphicsView.mousePressEvent(self, event)
|
382 |
|
383 |
'''
|
384 |
@brief
|
385 |
@author
|
386 |
@date
|
387 |
'''
|
388 |
|
389 |
def mouseReleaseEvent(self, event): |
390 |
try:
|
391 |
if self.command is not None: |
392 |
scenePos = self.mapToScene(event.pos())
|
393 |
instance = self.command.execute(['mouseReleaseEvent', event, scenePos]) |
394 |
if instance is not None: |
395 |
self.scene().addItem(instance)
|
396 |
|
397 |
if self.command is not None and self.command.isTreated == True: |
398 |
if self.command.name == 'Default' and self.command.isCopy: |
399 |
return
|
400 |
self.command = DefaultCommand.DefaultCommand(self) |
401 |
cursor = QCursor(Qt.ArrowCursor) |
402 |
QApplication.instance().setOverrideCursor(cursor) |
403 |
return
|
404 |
else:
|
405 |
QGraphicsView.mouseReleaseEvent(self, event)
|
406 |
except Exception as ex: |
407 |
from App import App |
408 |
from AppDocData import MessageType |
409 |
|
410 |
message = 'error occurred({}) in {}:{}'.format(repr(ex), sys.exc_info()[-1].tb_frame.f_code.co_filename, |
411 |
sys.exc_info()[-1].tb_lineno)
|
412 |
App.mainWnd().addMessage.emit(MessageType.Error, message) |
413 |
|
414 |
"""
|
415 |
@brief Show entire image.
|
416 |
"""
|
417 |
|
418 |
def mouseDoubleClickEvent(self, event): |
419 |
scenePos = self.mapToScene(event.pos())
|
420 |
if self.command is not None: |
421 |
instance = self.command.execute(['mouseDoubleClickEvent', event, scenePos]) |
422 |
if self.command.isTreated == True: return |
423 |
|
424 |
if event.button() == Qt.LeftButton:
|
425 |
self.leftMouseButtonDoubleClicked.emit(scenePos.x(), scenePos.y())
|
426 |
"""
|
427 |
elif event.button() == Qt.RightButton:
|
428 |
if self.canZoom:
|
429 |
self.zoomStack = [] # Clear zoom stack.
|
430 |
self.updateViewer()
|
431 |
self.rightMouseButtonDoubleClicked.emit(scenePos.x(), scenePos.y())
|
432 |
"""
|
433 |
|
434 |
QGraphicsView.mouseDoubleClickEvent(self, event)
|
435 |
|
436 |
'''
|
437 |
@brief key press event
|
438 |
@author Jeongwoo
|
439 |
@date 2018.??.??
|
440 |
@history send escape key event to command
|
441 |
'''
|
442 |
|
443 |
def keyPressEvent(self, event): |
444 |
from TrainingEditorDialog import QTrainingEditorDialog |
445 |
from TrainingSymbolEditorDialog import QTrainingSymbolEditorDialog |
446 |
|
447 |
try:
|
448 |
#print('view : ' + str(event.key()))
|
449 |
if event.key() == Qt.Key_Escape or event.key() == Qt.Key_Return: |
450 |
if self.command is not None: |
451 |
self.command.execute(['keyPressEvent', event, []]) |
452 |
if self.command.isTreated: |
453 |
self.command = DefaultCommand.DefaultCommand(self) |
454 |
return
|
455 |
else:
|
456 |
if self.command is not None: |
457 |
self.command.execute(['keyPressEvent', event, []]) |
458 |
if self.command.isTreated: return |
459 |
if type(self.mainWindow) is QTrainingEditorDialog or type(self.mainWindow) is QTrainingSymbolEditorDialog: |
460 |
self.mainWindow.keyPressEvent(event)
|
461 |
|
462 |
#print(QApplication.keyboardModifiers() == Qt.ControlModifier)
|
463 |
QGraphicsView.keyPressEvent(self, event)
|
464 |
except Exception as ex: |
465 |
message = f"error occurred({repr(ex)}) in {sys.exc_info()[-1].tb_frame.f_code.co_filename}:" \
|
466 |
f"{sys.exc_info()[-1].tb_lineno}"
|
467 |
|
468 |
'''
|
469 |
@brief key release event
|
470 |
@author Jeongwoo
|
471 |
@date 2018.??.??
|
472 |
'''
|
473 |
|
474 |
def keyReleaseEvent(self, event): |
475 |
if event.key() == Qt.Key_Delete:
|
476 |
pass
|
477 |
|
478 |
QGraphicsView.keyReleaseEvent(self, event)
|
479 |
|
480 |
'''
|
481 |
@brief mouse wheel event
|
482 |
@autor humkyung
|
483 |
@date
|
484 |
'''
|
485 |
|
486 |
def wheelEvent(self, event): |
487 |
if event.modifiers() == Qt.ControlModifier:
|
488 |
if self.canZoom and self.hasImage(): |
489 |
numDegrees = event.angleDelta() / 8
|
490 |
if numDegrees is not None: |
491 |
if numDegrees.y() > 0: |
492 |
self.zoomImage(True, event, 1.5) |
493 |
elif numDegrees.y() < 0: |
494 |
self.zoomImage(False, event, 0.5) |
495 |
else:
|
496 |
super().wheelEvent(event)
|
497 |
|
498 |
'''
|
499 |
@brief draw background
|
500 |
@author humkyung
|
501 |
@date 2018.07.23
|
502 |
'''
|
503 |
|
504 |
"""
|
505 |
def drawBackground(self, painter, rect):
|
506 |
QGraphicsView.drawBackground(self, painter, rect)
|
507 |
"""
|
508 |
|
509 |
'''
|
510 |
@history 2018.06.11 Jeongwoo Change method to manage guideline items
|
511 |
humkyung 2018.08.28 remove guide lines before drawing
|
512 |
'''
|
513 |
GUIDELINE_ITEMS = [] |
514 |
|
515 |
def showGuideline(self, pos, isShow): |
516 |
image = self.image()
|
517 |
width = image.width() |
518 |
height = image.height() |
519 |
pen = QPen() |
520 |
pen.setColor(QColor(180, 180, 180)) |
521 |
pen.setStyle(Qt.DashLine) |
522 |
pen.setWidthF(0.5)
|
523 |
if isShow:
|
524 |
items = self.scene().items()
|
525 |
for item in self.GUIDELINE_ITEMS: |
526 |
if item in items: |
527 |
self.scene().removeItem(item)
|
528 |
self.GUIDELINE_ITEMS.clear()
|
529 |
|
530 |
if pos is not None: |
531 |
verticalLine = self.scene().addLine(pos.x(), 0, pos.x(), height, pen) |
532 |
horizontalLine = self.scene().addLine(0, pos.y(), width, pos.y(), pen) |
533 |
else:
|
534 |
verticalLine = self.scene().addLine(round(width * 0.5), 0, round(width * 0.5), height, pen) |
535 |
horizontalLine = self.scene().addLine(0, round(height * 0.5), width, round(height * 0.5), pen) |
536 |
|
537 |
self.GUIDELINE_ITEMS.append(verticalLine)
|
538 |
self.GUIDELINE_ITEMS.append(horizontalLine)
|
539 |
else:
|
540 |
items = self.scene().items()
|
541 |
for item in self.GUIDELINE_ITEMS: |
542 |
if item in items: |
543 |
self.scene().removeItem(item)
|
544 |
self.GUIDELINE_ITEMS.clear()
|
545 |
|
546 |
'''
|
547 |
@brief drag enter event
|
548 |
@author humkyung
|
549 |
@date 2018.04.17
|
550 |
'''
|
551 |
|
552 |
def dragEnterEvent(self, event): |
553 |
event.acceptProposedAction() |
554 |
|
555 |
'''
|
556 |
@brief drag move event
|
557 |
@author humkyung
|
558 |
@date 2018.04.17
|
559 |
@history humkyung 2018.08.21 highlight item under mouse
|
560 |
'''
|
561 |
|
562 |
def dragMoveEvent(self, event): |
563 |
scenePos = self.mapToScene(event.pos())
|
564 |
items = [item for item in self.scene().items(scenePos) if |
565 |
type(item) is not QGraphicsPixmapItem and type(item) is not QGraphicsTextItem] |
566 |
if len(items) > 0: |
567 |
if not hasattr(self, '_underItem') or self._underItem is not items[0]: |
568 |
if hasattr(self, '_underItem') and self._underItem is not None: |
569 |
if hasattr(self._underItem, 'highlight') and self._underItem in self.scene().items(): |
570 |
self._underItem.highlight(False) |
571 |
else:
|
572 |
self._underItem = None |
573 |
|
574 |
self._underItem = items[0] |
575 |
if hasattr(self._underItem, 'highlight'): |
576 |
self._underItem.highlight(True) |
577 |
# elif hasattr(self, '_underItem') and self._underItem is not None:
|
578 |
# self._underItem.hoverLeaveEvent(event)
|
579 |
# self._underItem = None
|
580 |
|
581 |
event.acceptProposedAction() |
582 |
|
583 |
def dropEvent(self, event): |
584 |
"""drop a symbol"""
|
585 |
from AppDocData import AppDocData |
586 |
from EngineeringEndBreakItem import QEngineeringEndBreakItem |
587 |
from EngineeringSpecBreakItem import QEngineeringSpecBreakItem |
588 |
|
589 |
if len(self.scene().items()) is 0: |
590 |
return
|
591 |
if hasattr(self, '_underItem') and self._underItem is not None and self._underItem in self.scene().items(): |
592 |
self._underItem.hoverLeaveEvent(None) |
593 |
self._underItem = None |
594 |
else:
|
595 |
self._underItem = None |
596 |
|
597 |
for item in self.scene().selectedItems(): |
598 |
item.clearFocus() |
599 |
item.setSelected(False)
|
600 |
|
601 |
scenePos = self.mapToScene(event.pos())
|
602 |
name = event.mimeData().text() |
603 |
svg = QtImageViewer.createSymbolObject(name, event.mimeData().tag.getScale()) |
604 |
fixedAngle = 0.0 if QApplication.keyboardModifiers() == Qt.ControlModifier else None |
605 |
QtImageViewer.matchSymbolToLine(self.scene(), svg, scenePos, angle=fixedAngle)
|
606 |
if svg:
|
607 |
if type(svg) is QEngineeringEndBreakItem or type(svg) is QEngineeringSpecBreakItem: |
608 |
svg.set_property('Freeze', True) |
609 |
|
610 |
svg.setSelected(True)
|
611 |
svg.setFocus() |
612 |
self.setFocus() # set focus to graphicview |
613 |
|
614 |
event.acceptProposedAction() |
615 |
|
616 |
'''
|
617 |
@brief drop create Symbol
|
618 |
@author kyouho
|
619 |
@date 2018.07.27
|
620 |
'''
|
621 |
@staticmethod
|
622 |
def createSymbolObject(name, scale=[1.0, 1.0]): |
623 |
"""create a symbol object has given uid"""
|
624 |
from AppDocData import AppDocData |
625 |
|
626 |
app_doc_data = AppDocData.instance() |
627 |
|
628 |
symbol = app_doc_data.getSymbolByQuery('name', name)
|
629 |
if symbol:
|
630 |
svg_file_name = symbol.sName |
631 |
svgFilePath = os.path.join(app_doc_data.getCurrentProject().getSvgFilePath(), symbol.getType(), |
632 |
svg_file_name + '.svg')
|
633 |
svg = SymbolSvgItem.createItem(symbol.getType(), None, svgFilePath)
|
634 |
connPts = None
|
635 |
strConnPts = symbol.getConnectionPoint() |
636 |
if strConnPts is not None and strConnPts != '': |
637 |
connPts = [(float(x.split(',')[0]), float(x.split(',')[1])) if len(x.split(',')) == 2 else ( |
638 |
x.split(',')[0], float(x.split(',')[1]), float(x.split(',')[2])) \ |
639 |
for x in strConnPts.split('/')] |
640 |
|
641 |
svg.buildItem(svg_file_name, symbol.getType(), 0, None, [symbol.width, symbol.height], None, connPts, symbol.getBaseSymbol(), |
642 |
symbol.getAdditionalSymbol(), symbol.getHasInstrumentLabel(), None, scale)
|
643 |
|
644 |
return svg
|
645 |
else:
|
646 |
return None |
647 |
|
648 |
'''
|
649 |
@brief match symbol to line
|
650 |
@author kyouho
|
651 |
@date 2018.07.27
|
652 |
@history humkyung 2018.08.23 change scenePos to connector's center when symbol is placed on connector
|
653 |
'''
|
654 |
@staticmethod
|
655 |
def matchSymbolToLine(scene, svg, scenePos, angle=None, flip=None, strict=False, auto=False, move=False): |
656 |
from EngineeringConnectorItem import QEngineeringConnectorItem |
657 |
from EngineeringLineItem import QEngineeringLineItem |
658 |
from SymbolSvgItem import SymbolSvgItem |
659 |
from EngineeringUnknownItem import QEngineeringUnknownItem |
660 |
from EngineeringAbstractItem import QEngineeringAbstractItem |
661 |
|
662 |
import math |
663 |
from App import App |
664 |
from AppDocData import AppDocData |
665 |
|
666 |
try:
|
667 |
if not svg.scene(): |
668 |
svg.transfer.onRemoved.connect(App.mainWnd().itemRemoved) |
669 |
|
670 |
items = [item for item in scene.items(scenePos) if type(item) is not QEngineeringUnknownItem and \ |
671 |
type(item) is not QGraphicsPixmapItem and type(item) is not QGraphicsTextItem and item is not svg] |
672 |
connectors = [] |
673 |
if len(items) > 0 and type(items[0]) is QEngineeringConnectorItem: |
674 |
scenePos = QPointF(items[0].center()[0], items[0].center()[1]) |
675 |
connectors = [connector for connector in items if issubclass(type(connector.parentItem()), SymbolSvgItem) \ |
676 |
and type(connector) is QEngineeringConnectorItem] |
677 |
if not connectors: |
678 |
connectors = [connector for connector in items if type(connector.parentItem()) is QEngineeringLineItem \ |
679 |
and type(connector) is QEngineeringConnectorItem] |
680 |
|
681 |
#matches = [item for item in scene.items() if
|
682 |
# (type(item) is QEngineeringLineItem) and (item.distanceTo((scenePos.x(), scenePos.y())) < 20)]
|
683 |
allowed_error = 0.0001
|
684 |
if len(connectors) == 1 and len(svg.connectors) >= 2 and len(connectors[0].parentItem().connectors): |
685 |
if move:
|
686 |
svg.scene().removeItem(svg) |
687 |
svg.resetPosition() |
688 |
|
689 |
# item assistant with connection
|
690 |
xl = connectors[0].parentItem().symbolOrigin[0] - connectors[0].connectPoint[0] |
691 |
yl = connectors[0].parentItem().symbolOrigin[1] - connectors[0].connectPoint[1] |
692 |
length = math.sqrt(xl * xl + yl * yl) |
693 |
if length < allowed_error:
|
694 |
return
|
695 |
ddx = (connectors[0].sceneBoundingRect().center().x() - connectors[0].parentItem().origin[0]) / length |
696 |
ddy = (connectors[0].sceneBoundingRect().center().y() - connectors[0].parentItem().origin[1]) / length |
697 |
dx, dy = abs(svg.connectors[1].connectPoint[0] - svg.symbolOrigin[0]), abs(svg.connectors[1].connectPoint[1] - svg.symbolOrigin[1]) |
698 |
length = math.sqrt(dx * dx + dy * dy) |
699 |
if length < allowed_error:
|
700 |
return
|
701 |
dx, dy = length * ddx, length * ddy |
702 |
|
703 |
# if abs(connectors[0].parentItem().angle - math.pi / 2) < allowed_error or abs(connectors[0].parentItem().angle - math.pi / 2 * 3) < allowed_error:
|
704 |
# dx, dy = ddx * dy, ddy * dx
|
705 |
# else:
|
706 |
# dx, dy = ddx * dx, ddy * dy
|
707 |
|
708 |
xxl = connectors[0].parentItem().origin[0] - connectors[0].center()[0] |
709 |
yyl = connectors[0].parentItem().origin[1] - connectors[0].center()[1] |
710 |
rAngle = -math.atan2(yyl, xxl)# if flip == 0 else math.atan2(yl, xl)
|
711 |
rAngle = abs(rAngle) if rAngle < 0 + allowed_error else 2 * math.pi - rAngle |
712 |
if angle is not None: |
713 |
rAngle = angle |
714 |
svg.angle = rAngle |
715 |
|
716 |
x, y = connectors[0].sceneBoundingRect().center().x() + dx, \
|
717 |
connectors[0].sceneBoundingRect().center().y() + dy
|
718 |
svg.loc = [x - svg.symbolOrigin[0], y - svg.symbolOrigin[1]] |
719 |
svg.origin = [x, y] |
720 |
svg.addSvgItemToScene(scene, True if not auto else False) |
721 |
|
722 |
items = [item for item in scene.items(scenePos) if |
723 |
type(item) is not QGraphicsPixmapItem and type(item) is not QGraphicsTextItem] |
724 |
items = [item for item in items if item.parentItem() is svg and type(item) is QEngineeringConnectorItem] |
725 |
if items and connectors[0].connectedItem and type(connectors[0].connectedItem) is QEngineeringLineItem: |
726 |
items[0].connect(connectors[0].parentItem()) |
727 |
anotherConns = [conn for conn in svg.connectors if conn is not items[0]] |
728 |
anotherConns[0].connect(connectors[0].connectedItem) |
729 |
for lineConn in connectors[0].connectedItem.connectors: |
730 |
if lineConn.connectedItem is connectors[0].parentItem(): |
731 |
lineConn.connect(svg) |
732 |
lineConn.setPos(anotherConns[0].center())
|
733 |
break
|
734 |
connectors[0].connect(svg)
|
735 |
lineConn.transfer.onPosChanged.emit(lineConn) |
736 |
elif items:
|
737 |
items[0].connect(connectors[0].parentItem()) |
738 |
#items[0].highlight(False)
|
739 |
if connectors[0].connectedItem: |
740 |
for conn in connectors[0].connectedItem.connectors: |
741 |
if conn.connectedItem is connectors[0].parentItem(): |
742 |
conn.connect(None)
|
743 |
#conn.highlight(False)
|
744 |
break
|
745 |
connectors[0].connect(svg)
|
746 |
#connectors[0].highlight(False)
|
747 |
elif not strict and len(items) == 1 and type(items[0]) is QEngineeringLineItem and items[0].length() > svg.size[0] and items[0].length() > svg.size[1]: |
748 |
if move:
|
749 |
svg.scene().removeItem(svg) |
750 |
svg.resetPosition() |
751 |
|
752 |
vec = items[0].perpendicular()
|
753 |
line = [(scenePos.x() - vec[0] * 20, scenePos.y() - vec[1] * 20), (scenePos.x() + vec[0] * 20, scenePos.y() + vec[1] * 20)] |
754 |
origin = items[0].intersection(line)
|
755 |
configs = AppDocData.instance().getConfigs('Data', 'Grid') |
756 |
grid = int(configs[0].value) if 1 == len(configs) else -1 |
757 |
if grid == 1: |
758 |
svg.origin = [round(origin.x), round(origin.y)] |
759 |
svg.loc = [round(origin.x - svg.symbolOrigin[0]), round(origin.y - svg.symbolOrigin[1])] |
760 |
'''
|
761 |
if not svg.symbolConvertingOrigin:
|
762 |
svg.origin = [round(origin.x), round(origin.y)]
|
763 |
svg.loc = [round(origin.x - svg.symbolOrigin[0]), round(origin.y - svg.symbolOrigin[1])]
|
764 |
else:
|
765 |
svg.origin = [round(origin.x - svg.symbolConvertingOrigin[0] + svg.symbolOrigin[0]), round(origin.y - svg.symbolConvertingOrigin[1] + svg.symbolOrigin[1])]
|
766 |
svg.loc = [round(origin.x - svg.symbolConvertingOrigin[0]), round(origin.y - svg.symbolConvertingOrigin[1])]
|
767 |
'''
|
768 |
else:
|
769 |
svg.origin = [round(origin.x, 1), round(origin.y, 1)] |
770 |
svg.loc = [round(origin.x - svg.symbolOrigin[0], 1), round(origin.y - svg.symbolOrigin[1], 1)] |
771 |
'''
|
772 |
if not svg.symbolConvertingOrigin:
|
773 |
svg.origin = [round(origin.x, 1), round(origin.y, 1)]
|
774 |
svg.loc = [round(origin.x - svg.symbolOrigin[0], 1), round(origin.y - svg.symbolOrigin[1], 1)]
|
775 |
else:
|
776 |
svg.origin = [round(origin.x - svg.symbolConvertingOrigin[0] + svg.symbolOrigin[0], 1), round(origin.y - svg.symbolConvertingOrigin[1] + svg.symbolOrigin[1], 1)]
|
777 |
svg.loc = [round(origin.x - svg.symbolConvertingOrigin[0], 1), round(origin.y - svg.symbolConvertingOrigin[1], 1)]
|
778 |
'''
|
779 |
svg.angle = items[0].angle()
|
780 |
if items[0].start_point()[1] - items[0].end_point()[1] > 0: |
781 |
svg.angle = math.pi - svg.angle |
782 |
|
783 |
svg.addSvgItemToScene(scene, True if not auto else False, True) |
784 |
|
785 |
if len(svg.connectors) > 1: |
786 |
connectors = [svg.connectors[0], svg.connectors[1]] |
787 |
connectors.sort(key=lambda x: abs(x.center()[0] - items[0].connectors[0].center()[0]) + abs(x.center()[1] - items[0].connectors[0].center()[1])) |
788 |
|
789 |
savedConnectors = [] |
790 |
for _connector in items[0].connectors: |
791 |
if _connector.connectedItem and _connector._connected_at == QEngineeringAbstractItem.CONNECTED_AT_PT: |
792 |
_connectors2 = [_connector2 for _connector2 in _connector.connectedItem.connectors if _connector2.connectedItem is items[0] and _connector2._connected_at == QEngineeringAbstractItem.CONNECTED_AT_PT] |
793 |
if _connectors2:
|
794 |
savedConnectors.append(_connectors2[0])
|
795 |
continue
|
796 |
savedConnectors.append(None)
|
797 |
|
798 |
inLine = QEngineeringLineItem(vertices=[items[0].start_point(), connectors[0].center()]) |
799 |
inLine.transfer.onRemoved.connect(App.mainWnd().itemRemoved) |
800 |
inLine.lineType = items[0].lineType
|
801 |
|
802 |
outLine = QEngineeringLineItem(vertices=[connectors[1].center(), items[0].end_point()]) |
803 |
outLine.transfer.onRemoved.connect(App.mainWnd().itemRemoved) |
804 |
outLine.lineType = items[0].lineType
|
805 |
|
806 |
inLine.connectors[0].connect(items[0].connectors[0].connectedItem, items[0].connectors[0]._connected_at) |
807 |
inLine.connectors[1].connect(svg)
|
808 |
connectors[0].connect(inLine)
|
809 |
outLine.connectors[0].connect(svg)
|
810 |
outLine.connectors[1].connect(items[0].connectors[1].connectedItem, items[0].connectors[1]._connected_at) |
811 |
connectors[1].connect(outLine)
|
812 |
|
813 |
if savedConnectors[0]: |
814 |
savedConnectors[0].connect(inLine)
|
815 |
if savedConnectors[1]: |
816 |
savedConnectors[1].connect(outLine)
|
817 |
|
818 |
scene.addItem(inLine) |
819 |
scene.addItem(outLine) |
820 |
items[0].transfer.onRemoved.emit(items[0]) |
821 |
|
822 |
elif not strict and not move: |
823 |
svg.angle = angle if angle else 0.0 |
824 |
svg.flip = flip if flip else 0 |
825 |
configs = AppDocData.instance().getConfigs('Data', 'Grid') |
826 |
grid = int(configs[0].value) if 1 == len(configs) else -1 |
827 |
if grid == 1: |
828 |
svg.origin = [round(scenePos.x()), round(scenePos.y())] |
829 |
svg.loc = [round(scenePos.x() - svg.symbolOrigin[0]), round(scenePos.y() - svg.symbolOrigin[1])] |
830 |
'''
|
831 |
if not svg.symbolConvertingOrigin:
|
832 |
svg.origin = [round(scenePos.x()), round(scenePos.y())]
|
833 |
svg.loc = [round(scenePos.x() - svg.symbolOrigin[0]), round(scenePos.y() - svg.symbolOrigin[1])]
|
834 |
else:
|
835 |
svg.origin = [round(scenePos.x() - svg.symbolConvertingOrigin[0] + svg.symbolOrigin[0]), round(scenePos.y() - svg.symbolConvertingOrigin[1] + svg.symbolOrigin[1])]
|
836 |
svg.loc = [round(scenePos.x() - svg.symbolConvertingOrigin[0]), round(scenePos.y() - svg.symbolConvertingOrigin[1])]
|
837 |
'''
|
838 |
else:
|
839 |
svg.origin = [round(scenePos.x(), 1), round(scenePos.y(), 1)] |
840 |
svg.loc = [round(scenePos.x() - svg.symbolOrigin[0], 1), round(scenePos.y() - svg.symbolOrigin[1], 1)] |
841 |
'''
|
842 |
if not svg.symbolConvertingOrigin:
|
843 |
svg.origin = [round(scenePos.x(), 1), round(scenePos.y(), 1)]
|
844 |
svg.loc = [round(scenePos.x() - svg.symbolOrigin[0], 1), round(scenePos.y() - svg.symbolOrigin[1], 1)]
|
845 |
else:
|
846 |
svg.origin = [round(scenePos.x() - svg.symbolConvertingOrigin[0] + svg.symbolOrigin[0], 1), round(scenePos.y() - svg.symbolConvertingOrigin[1] + svg.symbolOrigin[1], 1)]
|
847 |
svg.loc = [round(scenePos.x() - svg.symbolConvertingOrigin[0], 1), round(scenePos.y() - svg.symbolConvertingOrigin[1], 1)]
|
848 |
'''
|
849 |
|
850 |
if len(svg.connectors) == 1: |
851 |
# single connection item assistant
|
852 |
connectors = [connector for connector in connectors if connector.parentItem() is not svg and not connector.connectedItem] |
853 |
|
854 |
if len(connectors) == 1: |
855 |
xxl = connectors[0].parentItem().origin[0] - connectors[0].center()[0] |
856 |
yyl = connectors[0].parentItem().origin[1] - connectors[0].center()[1] |
857 |
rAngle = -math.atan2(yyl, xxl) |
858 |
rAngle = abs(rAngle) if rAngle < 0 + allowed_error else 2 * math.pi - rAngle |
859 |
rAngle = rAngle + math.pi if rAngle + math.pi < 2 * math.pi else rAngle - math.pi |
860 |
svg.angle = rAngle |
861 |
|
862 |
|
863 |
svg.connectors[0].connect(connectors[0].parentItem()) |
864 |
#svg.connectors[0].highlight(False)
|
865 |
connectors[0].connect(svg)
|
866 |
#items[0].highlight(False)
|
867 |
|
868 |
svg.addSvgItemToScene(scene, True if not auto else False, True) |
869 |
|
870 |
# svg.reSettingConnetors()
|
871 |
|
872 |
'''
|
873 |
if not strict:
|
874 |
# need fix
|
875 |
App.mainWnd().symbolTreeWidget.clearFocus()
|
876 |
scene.setFocus()
|
877 |
#scene.clearFocus()
|
878 |
for item in scene.selectedItems():
|
879 |
item.setSelected(False)
|
880 |
|
881 |
svg.setSelected(True)
|
882 |
scene.setFocusItem(svg)
|
883 |
'''
|
884 |
except Exception as ex: |
885 |
from App import App |
886 |
from AppDocData import MessageType |
887 |
|
888 |
message = 'error occurred({}) in {}:{}'.format(ex, sys.exc_info()[-1].tb_frame.f_code.co_filename, sys.exc_info()[-1].tb_lineno) |
889 |
App.mainWnd().addMessage.emit(MessageType.Error, message) |
890 |
|
891 |
'''
|
892 |
@brief find item by uid (SymbolSvgItem 기반, QEngineeringConnectorItem 제외, QEngineeringLineItem 포함)
|
893 |
@author kyouho
|
894 |
@date 2018.07.31
|
895 |
'''
|
896 |
|
897 |
def findItemByUid(self, uid): |
898 |
from EngineeringConnectorItem import QEngineeringConnectorItem |
899 |
|
900 |
items = [item for item in self.scene().items() if hasattr(item, 'uid') and str(item.uid) == str(uid)] |
901 |
return items[0] if items else None |
902 |
|
903 |
def convertQImageToMat(self, incomingImage): |
904 |
'''Converts a QImage into an opencv MAT format'''
|
905 |
import numpy as np |
906 |
|
907 |
try:
|
908 |
incomingImage = incomingImage.convertToFormat(QImage.Format_RGBA8888) |
909 |
|
910 |
width = incomingImage.width() |
911 |
height = incomingImage.height() |
912 |
|
913 |
ptr = incomingImage.bits() |
914 |
ptr.setsize(incomingImage.byteCount()) |
915 |
arr = np.array(ptr).reshape(height, width, 4) # Copies the data |
916 |
return arr
|
917 |
except Exception as ex: |
918 |
print('error occurred({}) in {}:{}'.format(ex, sys.exc_info()[-1].tb_frame.f_code.co_filename, |
919 |
sys.exc_info()[-1].tb_lineno))
|
920 |
|
921 |
|
922 |
if __name__ == '__main__': |
923 |
import sys |
924 |
|
925 |
try:
|
926 |
from PyQt5.QtWidgets import QApplication |
927 |
except ImportError: |
928 |
try:
|
929 |
from PyQt4.QtGui import QApplication |
930 |
except ImportError: |
931 |
raise ImportError("ImageViewerQt: Requires PyQt5 or PyQt4.") |
932 |
print('Using Qt ' + QT_VERSION_STR)
|
933 |
|
934 |
|
935 |
def handleLeftClick(x, y): |
936 |
row = int(y)
|
937 |
column = int(x)
|
938 |
print("Clicked on image pixel (row=" + str(row) + ", column=" + str(column) + ")") |
939 |
|
940 |
|
941 |
# Create the application.
|
942 |
app = QApplication(sys.argv) |
943 |
|
944 |
# Create image viewer and load an image file to display.
|
945 |
viewer = QtImageViewer(None)
|
946 |
viewer.loadImageFromFile() # Pops up file dialog.
|
947 |
|
948 |
# Handle left mouse clicks with custom slot.
|
949 |
viewer.leftMouseButtonPressed.connect(handleLeftClick) |
950 |
|
951 |
# Show viewer and run application.
|
952 |
viewer.show() |
953 |
sys.exit(app.exec_()) |