eric6/E5Graphics/E5GraphicsView.py

changeset 6942
2602857055c5
parent 6645
ad476851d7e0
child 7229
53054eb5b15a
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2007 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a canvas view class.
8 """
9
10 from __future__ import unicode_literals
11
12 import sys
13
14 from PyQt5.QtCore import pyqtSignal, QRectF, QSize, QSizeF, Qt
15 from PyQt5.QtGui import QBrush, QPainter, QPixmap, QFont, QColor
16 from PyQt5.QtWidgets import QGraphicsView
17
18 import Preferences
19
20
21 class E5GraphicsView(QGraphicsView):
22 """
23 Class implementing a graphics view.
24
25 @signal zoomValueChanged(int) emitted to signal a change of the zoom value
26 """
27 zoomValueChanged = pyqtSignal(int)
28
29 ZoomLevels = [
30 1, 3, 5, 7, 9,
31 10, 20, 30, 50, 67, 80, 90,
32 100,
33 110, 120, 133, 150, 170, 200, 240, 300, 400,
34 500, 600, 700, 800, 900, 1000,
35 ]
36 ZoomLevelDefault = 100
37
38 def __init__(self, scene, parent=None):
39 """
40 Constructor
41
42 @param scene reference to the scene object (QGraphicsScene)
43 @param parent parent widget (QWidget)
44 """
45 super(E5GraphicsView, self).__init__(scene, parent)
46 self.setObjectName("E5GraphicsView")
47
48 self.__initialSceneSize = self.scene().sceneRect().size()
49 self.setBackgroundBrush(QBrush(Qt.white))
50 self.setRenderHint(QPainter.Antialiasing, True)
51 self.setDragMode(QGraphicsView.RubberBandDrag)
52 self.setAlignment(Qt.Alignment(Qt.AlignLeft | Qt.AlignTop))
53 self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
54 self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
55 self.setViewportUpdateMode(QGraphicsView.SmartViewportUpdate)
56
57 self.setWhatsThis(self.tr(
58 "<b>Graphics View</b>\n"
59 "<p>This graphics view is used to show a diagram. \n"
60 "There are various actions available to manipulate the \n"
61 "shown items.</p>\n"
62 "<ul>\n"
63 "<li>Clicking on an item selects it.</li>\n"
64 "<li>Ctrl-clicking adds an item to the selection.</li>\n"
65 "<li>Ctrl-clicking a selected item deselects it.</li>\n"
66 "<li>Clicking on an empty spot of the canvas resets the selection."
67 "</li>\n"
68 "<li>Dragging the mouse over the canvas spans a rubberband to \n"
69 "select multiple items.</li>\n"
70 "<li>Dragging the mouse over a selected item moves the \n"
71 "whole selection.</li>\n"
72 "</ul>\n"
73 ))
74
75 def __levelForZoom(self, zoom):
76 """
77 Private method determining the zoom level index given a zoom factor.
78
79 @param zoom zoom factor (integer)
80 @return index of zoom factor (integer)
81 """
82 try:
83 index = E5GraphicsView.ZoomLevels.index(zoom)
84 except ValueError:
85 for index in range(len(E5GraphicsView.ZoomLevels)):
86 if zoom <= E5GraphicsView.ZoomLevels[index]:
87 break
88 return index
89
90 def zoomIn(self):
91 """
92 Public method to zoom in.
93 """
94 index = self.__levelForZoom(self.zoom())
95 if index < len(E5GraphicsView.ZoomLevels) - 1:
96 self.setZoom(E5GraphicsView.ZoomLevels[index + 1])
97
98 def zoomOut(self):
99 """
100 Public method to zoom out.
101 """
102 index = self.__levelForZoom(self.zoom())
103 if index > 0:
104 self.setZoom(E5GraphicsView.ZoomLevels[index - 1])
105
106 def zoomReset(self):
107 """
108 Public method to handle the reset the zoom value.
109 """
110 self.setZoom(
111 E5GraphicsView.ZoomLevels[E5GraphicsView.ZoomLevelDefault])
112
113 def setZoom(self, value):
114 """
115 Public method to set the zoom value in percent.
116
117 @param value zoom value in percent (integer)
118 """
119 if value != self.zoom():
120 self.resetTransform()
121 factor = value / 100.0
122 self.scale(factor, factor)
123 self.zoomValueChanged.emit(value)
124
125 def zoom(self):
126 """
127 Public method to get the current zoom factor in percent.
128
129 @return current zoom factor in percent (integer)
130 """
131 return int(self.transform().m11() * 100.0)
132
133 def resizeScene(self, amount, isWidth=True):
134 """
135 Public method to resize the scene.
136
137 @param amount size increment (integer)
138 @param isWidth flag indicating width is to be resized (boolean)
139 """
140 sceneRect = self.scene().sceneRect()
141 width = sceneRect.width()
142 height = sceneRect.height()
143 if isWidth:
144 width += amount
145 else:
146 height += amount
147 rect = self._getDiagramRect(10)
148 if width < rect.width():
149 width = rect.width()
150 if height < rect.height():
151 height = rect.height()
152
153 self.setSceneSize(width, height)
154
155 def setSceneSize(self, width, height):
156 """
157 Public method to set the scene size.
158
159 @param width width for the scene (real)
160 @param height height for the scene (real)
161 """
162 rect = self.scene().sceneRect()
163 rect.setHeight(height)
164 rect.setWidth(width)
165 self.scene().setSceneRect(rect)
166
167 def autoAdjustSceneSize(self, limit=False):
168 """
169 Public method to adjust the scene size to the diagram size.
170
171 @param limit flag indicating to limit the scene to the
172 initial size (boolean)
173 """
174 size = self._getDiagramSize(10)
175 if limit:
176 newWidth = max(size.width(), self.__initialSceneSize.width())
177 newHeight = max(size.height(), self.__initialSceneSize.height())
178 else:
179 newWidth = size.width()
180 newHeight = size.height()
181 self.setSceneSize(newWidth, newHeight)
182
183 def _getDiagramRect(self, border=0):
184 """
185 Protected method to calculate the minimum rectangle fitting the
186 diagram.
187
188 @param border border width to include in the calculation (integer)
189 @return the minimum rectangle (QRectF)
190 """
191 startx = sys.maxsize
192 starty = sys.maxsize
193 endx = 0
194 endy = 0
195 items = self.filteredItems(list(self.scene().items()))
196 for itm in items:
197 rect = itm.sceneBoundingRect()
198 itmEndX = rect.x() + rect.width()
199 itmEndY = rect.y() + rect.height()
200 itmStartX = rect.x()
201 itmStartY = rect.y()
202 if startx >= itmStartX:
203 startx = itmStartX
204 if starty >= itmStartY:
205 starty = itmStartY
206 if endx <= itmEndX:
207 endx = itmEndX
208 if endy <= itmEndY:
209 endy = itmEndY
210 if border:
211 startx -= border
212 starty -= border
213 endx += border
214 endy += border
215
216 return QRectF(startx, starty, endx - startx + 1, endy - starty + 1)
217
218 def _getDiagramSize(self, border=0):
219 """
220 Protected method to calculate the minimum size fitting the diagram.
221
222 @param border border width to include in the calculation (integer)
223 @return the minimum size (QSizeF)
224 """
225 endx = 0
226 endy = 0
227 items = self.filteredItems(list(self.scene().items()))
228 for itm in items:
229 rect = itm.sceneBoundingRect()
230 itmEndX = rect.x() + rect.width()
231 itmEndY = rect.y() + rect.height()
232 if endx <= itmEndX:
233 endx = itmEndX
234 if endy <= itmEndY:
235 endy = itmEndY
236 if border:
237 endx += border
238 endy += border
239
240 return QSizeF(endx + 1, endy + 1)
241
242 def __getDiagram(self, rect, imageFormat="PNG", filename=None):
243 """
244 Private method to retrieve the diagram from the scene fitting it
245 in the minimum rectangle.
246
247 @param rect minimum rectangle fitting the diagram (QRectF)
248 @param imageFormat format for the image file (string)
249 @param filename name of the file for non pixmaps (string)
250 @return diagram pixmap to receive the diagram (QPixmap)
251 """
252 selectedItems = self.scene().selectedItems()
253
254 # step 1: deselect all widgets
255 if selectedItems:
256 for item in selectedItems:
257 item.setSelected(False)
258
259 # step 2: grab the diagram
260 if imageFormat == "PNG":
261 paintDevice = QPixmap(int(rect.width()), int(rect.height()))
262 paintDevice.fill(self.backgroundBrush().color())
263 else:
264 from PyQt5.QtSvg import QSvgGenerator
265 paintDevice = QSvgGenerator()
266 paintDevice.setResolution(100) # 100 dpi
267 paintDevice.setSize(QSize(int(rect.width()), int(rect.height())))
268 paintDevice.setViewBox(rect)
269 paintDevice.setFileName(filename)
270 painter = QPainter(paintDevice)
271 painter.setRenderHint(QPainter.Antialiasing, True)
272 self.scene().render(painter, QRectF(), rect)
273
274 # step 3: reselect the widgets
275 if selectedItems:
276 for item in selectedItems:
277 item.setSelected(True)
278
279 return paintDevice
280
281 def saveImage(self, filename, imageFormat="PNG"):
282 """
283 Public method to save the scene to a file.
284
285 @param filename name of the file to write the image to (string)
286 @param imageFormat format for the image file (string)
287 @return flag indicating success (boolean)
288 """
289 rect = self._getDiagramRect(self.border)
290 if imageFormat == "SVG":
291 self.__getDiagram(rect, imageFormat=imageFormat, filename=filename)
292 return True
293 else:
294 pixmap = self.__getDiagram(rect)
295 return pixmap.save(filename, imageFormat)
296
297 def printDiagram(self, printer, diagramName=""):
298 """
299 Public method to print the diagram.
300
301 @param printer reference to a ready configured printer object
302 (QPrinter)
303 @param diagramName name of the diagram (string)
304 """
305 painter = QPainter()
306 painter.begin(printer)
307 offsetX = 0
308 offsetY = 0
309 widthX = 0
310 heightY = 0
311 font = QFont("times", 10)
312 painter.setFont(font)
313 fm = painter.fontMetrics()
314 fontHeight = fm.lineSpacing()
315 marginX = printer.pageRect().x() - printer.paperRect().x()
316 marginX = \
317 Preferences.getPrinter("LeftMargin") * int(
318 printer.resolution() / 2.54) - marginX
319 marginY = printer.pageRect().y() - printer.paperRect().y()
320 marginY = \
321 Preferences.getPrinter("TopMargin") * int(
322 printer.resolution() / 2.54) - marginY
323
324 width = printer.width() - marginX \
325 - Preferences.getPrinter("RightMargin") * int(
326 printer.resolution() / 2.54)
327 height = printer.height() - fontHeight - 4 - marginY \
328 - Preferences.getPrinter("BottomMargin") * int(
329 printer.resolution() / 2.54)
330
331 border = self.border == 0 and 5 or self.border
332 rect = self._getDiagramRect(border)
333 diagram = self.__getDiagram(rect)
334
335 finishX = False
336 finishY = False
337 page = 0
338 pageX = 0
339 pageY = 1
340 while not finishX or not finishY:
341 if not finishX:
342 offsetX = pageX * width
343 pageX += 1
344 elif not finishY:
345 offsetY = pageY * height
346 offsetX = 0
347 pageY += 1
348 finishX = False
349 pageX = 1
350 if (width + offsetX) > diagram.width():
351 finishX = True
352 widthX = diagram.width() - offsetX
353 else:
354 widthX = width
355 if diagram.width() < width:
356 widthX = diagram.width()
357 finishX = True
358 offsetX = 0
359 if (height + offsetY) > diagram.height():
360 finishY = True
361 heightY = diagram.height() - offsetY
362 else:
363 heightY = height
364 if diagram.height() < height:
365 finishY = True
366 heightY = diagram.height()
367 offsetY = 0
368
369 painter.drawPixmap(marginX, marginY, diagram,
370 offsetX, offsetY, widthX, heightY)
371 # write a foot note
372 s = self.tr("{0}, Page {1}").format(diagramName, page + 1)
373 tc = QColor(50, 50, 50)
374 painter.setPen(tc)
375 painter.drawRect(marginX, marginY, width, height)
376 painter.drawLine(marginX, marginY + height + 2,
377 marginX + width, marginY + height + 2)
378 painter.setFont(font)
379 painter.drawText(marginX, marginY + height + 4, width,
380 fontHeight, Qt.AlignRight, s)
381 if not finishX or not finishY:
382 printer.newPage()
383 page += 1
384
385 painter.end()
386
387 ###########################################################################
388 ## The methods below should be overridden by subclasses to get special
389 ## behavior.
390 ###########################################################################
391
392 def filteredItems(self, items):
393 """
394 Public method to filter a list of items.
395
396 @param items list of items as returned by the scene object
397 (QGraphicsItem)
398 @return list of interesting collision items (QGraphicsItem)
399 """
400 # just return the list unchanged
401 return items

eric ide

mercurial