1#!/usr/bin/env python 2 3 4############################################################################# 5## 6## Copyright (C) 2017 Riverbank Computing Limited. 7## Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). 8## All rights reserved. 9## 10## This file is part of the examples of PyQt. 11## 12## $QT_BEGIN_LICENSE:BSD$ 13## You may use this file under the terms of the BSD license as follows: 14## 15## "Redistribution and use in source and binary forms, with or without 16## modification, are permitted provided that the following conditions are 17## met: 18## * Redistributions of source code must retain the above copyright 19## notice, this list of conditions and the following disclaimer. 20## * Redistributions in binary form must reproduce the above copyright 21## notice, this list of conditions and the following disclaimer in 22## the documentation and/or other materials provided with the 23## distribution. 24## * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor 25## the names of its contributors may be used to endorse or promote 26## products derived from this software without specific prior written 27## permission. 28## 29## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 30## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 31## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 32## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 33## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 34## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 35## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 36## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 37## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 38## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 39## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." 40## $QT_END_LICENSE$ 41## 42############################################################################# 43 44 45from PyQt5.QtCore import (QAbstractTableModel, QDir, QModelIndex, QRect, 46 QRectF, QSize, Qt) 47from PyQt5.QtGui import QBrush, qGray, QImage, QPainter 48from PyQt5.QtPrintSupport import QPrintDialog, QPrinter 49from PyQt5.QtWidgets import (QAbstractItemDelegate, QApplication, QDialog, 50 QFileDialog, QHBoxLayout, QLabel, QMainWindow, QMessageBox, QMenu, 51 QProgressDialog, QSpinBox, QStyle, QStyleOptionViewItem, QTableView, 52 QVBoxLayout, QWidget) 53 54import pixelator_rc 55 56 57ItemSize = 256 58 59 60class PixelDelegate(QAbstractItemDelegate): 61 def __init__(self, parent=None): 62 super(PixelDelegate, self).__init__(parent) 63 64 self.pixelSize = 12 65 66 def paint(self, painter, option, index): 67 if option.state & QStyle.State_Selected: 68 painter.fillRect(option.rect, option.palette.highlight()) 69 70 size = min(option.rect.width(), option.rect.height()) 71 brightness = index.model().data(index, Qt.DisplayRole) 72 radius = (size/2.0) - (brightness/255.0 * size/2.0) 73 if radius == 0.0: 74 return 75 76 painter.save() 77 painter.setRenderHint(QPainter.Antialiasing) 78 painter.setPen(Qt.NoPen) 79 80 if option.state & QStyle.State_Selected: 81 painter.setBrush(option.palette.highlightedText()) 82 else: 83 painter.setBrush(QBrush(Qt.black)) 84 85 painter.drawEllipse(QRectF( 86 option.rect.x() + option.rect.width()/2 - radius, 87 option.rect.y() + option.rect.height()/2 - radius, 88 2*radius, 2*radius)) 89 90 painter.restore() 91 92 def sizeHint(self, option, index): 93 return QSize(self.pixelSize, self.pixelSize) 94 95 def setPixelSize(self, size): 96 self.pixelSize = size 97 98 99class ImageModel(QAbstractTableModel): 100 def __init__(self, parent=None): 101 super(ImageModel, self).__init__(parent) 102 103 self.modelImage = QImage() 104 105 def setImage(self, image): 106 self.beginResetModel() 107 self.modelImage = QImage(image) 108 self.endResetModel() 109 110 def rowCount(self, parent): 111 return self.modelImage.height() 112 113 def columnCount(self, parent): 114 return self.modelImage.width() 115 116 def data(self, index, role): 117 if not index.isValid() or role != Qt.DisplayRole: 118 return None 119 120 return qGray(self.modelImage.pixel(index.column(), index.row())) 121 122 def headerData(self, section, orientation, role): 123 if role == Qt.SizeHintRole: 124 return QSize(1, 1) 125 126 return None 127 128 129class MainWindow(QMainWindow): 130 def __init__(self): 131 super(MainWindow, self).__init__() 132 133 self.currentPath = QDir.homePath() 134 self.model = ImageModel(self) 135 136 centralWidget = QWidget() 137 138 self.view = QTableView() 139 self.view.setShowGrid(False) 140 self.view.horizontalHeader().hide() 141 self.view.verticalHeader().hide() 142 self.view.horizontalHeader().setMinimumSectionSize(1) 143 self.view.verticalHeader().setMinimumSectionSize(1) 144 self.view.setModel(self.model) 145 146 delegate = PixelDelegate(self) 147 self.view.setItemDelegate(delegate) 148 149 pixelSizeLabel = QLabel("Pixel size:") 150 pixelSizeSpinBox = QSpinBox() 151 pixelSizeSpinBox.setMinimum(4) 152 pixelSizeSpinBox.setMaximum(32) 153 pixelSizeSpinBox.setValue(12) 154 155 fileMenu = QMenu("&File", self) 156 openAction = fileMenu.addAction("&Open...") 157 openAction.setShortcut("Ctrl+O") 158 159 self.printAction = fileMenu.addAction("&Print...") 160 self.printAction.setEnabled(False) 161 self.printAction.setShortcut("Ctrl+P") 162 163 quitAction = fileMenu.addAction("E&xit") 164 quitAction.setShortcut("Ctrl+Q") 165 166 helpMenu = QMenu("&Help", self) 167 aboutAction = helpMenu.addAction("&About") 168 169 self.menuBar().addMenu(fileMenu) 170 self.menuBar().addSeparator() 171 self.menuBar().addMenu(helpMenu) 172 173 openAction.triggered.connect(self.chooseImage) 174 self.printAction.triggered.connect(self.printImage) 175 quitAction.triggered.connect(QApplication.instance().quit) 176 aboutAction.triggered.connect(self.showAboutBox) 177 pixelSizeSpinBox.valueChanged.connect(delegate.setPixelSize) 178 pixelSizeSpinBox.valueChanged.connect(self.updateView) 179 180 controlsLayout = QHBoxLayout() 181 controlsLayout.addWidget(pixelSizeLabel) 182 controlsLayout.addWidget(pixelSizeSpinBox) 183 controlsLayout.addStretch(1) 184 185 mainLayout = QVBoxLayout() 186 mainLayout.addWidget(self.view) 187 mainLayout.addLayout(controlsLayout) 188 centralWidget.setLayout(mainLayout) 189 190 self.setCentralWidget(centralWidget) 191 192 self.setWindowTitle("Pixelator") 193 self.resize(640, 480) 194 195 def chooseImage(self): 196 fileName, _ = QFileDialog.getOpenFileName(self, "Choose an Image", 197 self.currentPath, '*') 198 199 if fileName: 200 self.openImage(fileName) 201 202 def openImage(self, fileName): 203 image = QImage() 204 205 if image.load(fileName): 206 self.model.setImage(image) 207 208 if not fileName.startswith(':/'): 209 self.currentPath = fileName 210 self.setWindowTitle("%s - Pixelator" % self.currentPath) 211 212 self.printAction.setEnabled(True) 213 self.updateView() 214 215 def printImage(self): 216 if self.model.rowCount(QModelIndex()) * self.model.columnCount(QModelIndex()) > 90000: 217 answer = QMessageBox.question(self, "Large Image Size", 218 "The printed image may be very large. Are you sure that " 219 "you want to print it?", 220 QMessageBox.Yes | QMessageBox.No) 221 if answer == QMessageBox.No: 222 return 223 224 printer = QPrinter(QPrinter.HighResolution) 225 226 dlg = QPrintDialog(printer, self) 227 dlg.setWindowTitle("Print Image") 228 229 if dlg.exec_() != QDialog.Accepted: 230 return 231 232 painter = QPainter() 233 painter.begin(printer) 234 235 rows = self.model.rowCount(QModelIndex()) 236 columns = self.model.columnCount(QModelIndex()) 237 sourceWidth = (columns+1) * ItemSize 238 sourceHeight = (rows+1) * ItemSize 239 240 painter.save() 241 242 xscale = printer.pageRect().width() / float(sourceWidth) 243 yscale = printer.pageRect().height() / float(sourceHeight) 244 scale = min(xscale, yscale) 245 246 painter.translate(printer.paperRect().x()+printer.pageRect().width()/2, 247 printer.paperRect().y()+printer.pageRect().height()/2) 248 painter.scale(scale, scale) 249 painter.translate(-sourceWidth/2, -sourceHeight/2) 250 251 option = QStyleOptionViewItem() 252 parent = QModelIndex() 253 254 progress = QProgressDialog("Printing...", "Cancel", 0, rows, self) 255 progress.setWindowModality(Qt.ApplicationModal) 256 y = ItemSize / 2.0 257 258 for row in range(rows): 259 progress.setValue(row) 260 QApplication.processEvents() 261 if progress.wasCanceled(): 262 break 263 264 x = ItemSize / 2.0 265 266 for column in range(columns): 267 option.rect = QRect(x, y, ItemSize, ItemSize) 268 self.view.itemDelegate().paint(painter, option, 269 self.model.index(row, column, parent)) 270 x += ItemSize 271 272 y += ItemSize 273 274 progress.setValue(rows) 275 276 painter.restore() 277 painter.end() 278 279 if progress.wasCanceled(): 280 QMessageBox.information(self, "Printing canceled", 281 "The printing process was canceled.", QMessageBox.Cancel) 282 283 def showAboutBox(self): 284 QMessageBox.about(self, "About the Pixelator example", 285 "This example demonstrates how a standard view and a custom\n" 286 "delegate can be used to produce a specialized " 287 "representation\nof data in a simple custom model.") 288 289 def updateView(self): 290 self.view.resizeColumnsToContents() 291 self.view.resizeRowsToContents() 292 293 294if __name__ == '__main__': 295 296 import sys 297 298 app = QApplication(sys.argv) 299 window = MainWindow() 300 window.show() 301 window.openImage(':/images/qt.png') 302 sys.exit(app.exec_()) 303