上接 Qt5 繪制圖形
1 圖像類
在 PyQt5/PySide2 中常用的圖像類有 4 個购啄,即 QPixmap、QImage截碴、QPicture 和 QBitmap:
- QPixmap 是專門為繪圖而設計的猖任,在繪制圖片時需要使用 QPixmap补履。
- QImage 提供了一個與硬件無關(guān)的圖像表示函數(shù)瞬沦,可以用于圖片的像素級訪問太伊。
- QPicture 是一個繪圖設備類,它繼承自 QPainter 類逛钻×沤梗可以使用 QPainter 的
begin()
函數(shù)在 QPicture 上繪圖,使用end()
函數(shù)結(jié)束繪圖绣的,使用 QPicture 的save()
函數(shù)將 QPainter 所使用過的繪圖指令保存到文件中叠赐。 - QBitmap 是一個繼承自 QPixmap 的簡單類,它提供了 1 bit 深度的二值圖像的類屡江。QBitmap 提供的單色圖像芭概,可以用來制作游標(QCursor)或者筆刷(QBrush)。
1.1 QPixmap
QPixmap 類用于繪圖設備的圖像顯示惩嘉,它可以作為一個 QPaintDevice 對象罢洲,也可以加載到一個控件中,通常是標簽或按鈕文黎,用于在標簽或按鈕上顯示圖像惹苗。
QPixmap 可以讀取的圖像文件類型有 BMP、GIF耸峭、JPG桩蓉、JPEG、PNG劳闹、PBM院究、PGM、PPM本涕、XBM业汰、XPM等。
QPixmap 類中的常用方法:
from xinet.Qt.qt5 import QtCore, QtGui, QtWidgets
from xinet.run_qt import run
class Window(QtWidgets.QWidget):
def __init__(self, image_path):
super().__init__()
lab1 = QtWidgets.QLabel()
lab1.setPixmap(QtGui.QPixmap(image_path))
lab1.setScaledContents(True) # 讓圖片自適應 label 大小
vbox = QtWidgets.QVBoxLayout()
vbox.addWidget(lab1)
self.setLayout(vbox)
self.setWindowTitle("QPixmap 例子")
# lbl.setPixmap(QPixmap("")) #移除label上的圖片
if __name__ == '__main__':
image_path = r'D:\share\python.jpg'
run(Window, image_path)
效果:
等比例縮放(這塊暫時沒有研究菩颖,下面是別人的代碼):
class LabelFrame(QLabel):
def __init__(self, parent=None):
super().__init__()
self.main_window = parent
self.setAlignment(Qt.AlignCenter) # 居中顯示
self.setMinimumSize(640, 480)
# self.setScaledContents(True)
def update_frame(self, frame):
if self.height() > self.width():
width = self.width()
height = int(frame.shape[0] * (width / frame.shape[1]))
else:
height = self.height()
width = int(frame.shape[1] * (height / frame.shape[0]))
frame = cv2.resize(frame, (width, height))
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # bgr -> rgb
h, w, c = frame.shape # 獲取圖片形狀
image = QImage(frame, w, h, 3 * w, QImage.Format_RGB888)
pix_map = QPixmap.fromImage(image)
self.setPixmap(pix_map)
1.2 鼠標繪圖
下面的代碼重寫了 QtWidgets.QMainWindow 的鼠標左鍵移動事件样漆,令鼠標移動畫出點:
from xinet.Qt.qt5 import QtCore, QtGui, QtWidgets
from xinet.run_qt import run
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.label = QtWidgets.QLabel()
canvas = QtGui.QPixmap(800, 800)
self.label.setPixmap(canvas)
self.setCentralWidget(self.label)
def mouseMoveEvent(self, e):
painter = QtGui.QPainter(self.label.pixmap())
painter.setPen(QtCore.Qt.green)
painter.drawPoint(e.x(), e.y())
painter.end()
self.update()
if __name__ == '__main__':
run(MainWindow)
效果:
雖然,一定程度上實現(xiàn)了鼠標繪圖晦闰,但是放祟,鼠標移動的速度很快的話,點之間的間隔會很大呻右。為此舞竿,需要使用 line 代替 point 來記錄鼠標移動的軌跡。利用變量 self.last_x
, self.last_y
記錄鼠標的最新位置窿冯,且鼠標釋放釋放變量:
from xinet.Qt.qt5 import QtCore, QtGui, QtWidgets
from xinet.run_qt import run
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.label = QtWidgets.QLabel()
canvas = QtGui.QPixmap(800, 800)
self.label.setPixmap(canvas)
self.setCentralWidget(self.label)
self.last_x, self.last_y = None, None
def mouseMoveEvent(self, e):
if self.last_x is None: # First event.
self.last_x = e.x()
self.last_y = e.y()
return # Ignore the first time.
painter = QtGui.QPainter(self.label.pixmap())
painter.setPen(QtCore.Qt.green)
painter.drawLine(self.last_x, self.last_y, e.x(), e.y())
painter.end()
self.update()
# Update the origin for next time.
self.last_x = e.x()
self.last_y = e.y()
def mouseReleaseEvent(self, e):
self.last_x = None
self.last_y = None
if __name__ == '__main__':
run(MainWindow)
效果:
效果還是不錯的。但該代碼存在冗余确徙,我們借助 QPoint 進一步簡化代碼(為了讓涂鴉的線更平滑醒串,加入了反走樣):
from xinet.Qt.qt5 import QtCore, QtGui, QtWidgets
from xinet.run_qt import run
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.label = QtWidgets.QLabel() # 設置占位符
canvas = QtGui.QPixmap(800, 800)
canvas.fill(QtGui.QColor(200, 200, 20, 50)) # 設置畫布的背景
#self.label.setScaledContents(True) # 讓畫布自適應 label 大小
self.label.setPixmap(canvas)
self.setCentralWidget(self.label)
self.lastPoint = QtCore.QPoint()
def draw_something(self, event):
painter = QtGui.QPainter(self.label.pixmap())
painter.setPen(QtCore.Qt.green)
painter.setRenderHint(QtGui.QPainter.Antialiasing) # 設定反走樣
painter.drawLine(self.lastPoint, event.pos())
painter.end()
self.update()
def mouseMoveEvent(self, event):
'''重寫鼠標移動事件'''
if self.lastPoint.isNull():
self.lastPoint = event.pos()
else:
self.draw_something(event)
# Update the origin for next time.
self.lastPoint = event.pos()
def mouseReleaseEvent(self, e):
self.lastPoint = QtCore.QPoint()
if __name__ == '__main__':
run(MainWindow)
畫筆的顏色只有一種执桌,為此,可以添加一個調(diào)色板(palette):
from xinet.Qt.qt5 import QtCore, QtGui, QtWidgets
from xinet.run_qt import run
COLORS = [
# 17 undertones https://lospec.com/palette-list/17undertones
'#000000', '#141923', '#414168', '#3a7fa7', '#35e3e3', '#8fd970', '#5ebb49',
'#458352', '#dcd37b', '#fffee5', '#ffd035', '#cc9245', '#a15c3e', '#a42f3b',
'#f45b7a', '#c24998', '#81588d', '#bcb0c2', '#ffffff',
]
class Canvas(QtWidgets.QLabel): # 設置占位符
def __init__(self):
super().__init__()
pixmap = QtGui.QPixmap(800, 600)
# 設置畫布背景
pixmap.fill(QtGui.QColor(200, 200, 20, 50))
self.setPixmap(pixmap)
self.lastPoint = QtCore.QPoint()
self.pen_color = QtGui.QColor('white')
def set_pen_color(self, c):
self.pen_color = QtGui.QColor(c)
def draw_something(self, event):
painter = QtGui.QPainter(self.pixmap())
p = painter.pen()
p.setWidth(2)
p.setColor(self.pen_color)
painter.setPen(p)
painter.setRenderHint(QtGui.QPainter.Antialiasing) # 設定反走樣
painter.drawLine(self.lastPoint, event.pos())
painter.end()
self.update()
def mouseMoveEvent(self, event):
'''重寫鼠標移動事件'''
if self.lastPoint.isNull():
self.lastPoint = event.pos()
else:
self.draw_something(event)
# Update the origin for next time.
self.lastPoint = event.pos()
def mouseReleaseEvent(self, e):
self.lastPoint = QtCore.QPoint()
class QPaletteButton(QtWidgets.QPushButton):
def __init__(self, color):
super().__init__()
self.setFixedSize(QtCore.QSize(24, 24))
self.color = color
self.setStyleSheet(f"background-color: {color};")
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.canvas = Canvas()
w = QtWidgets.QWidget()
l = QtWidgets.QVBoxLayout()
w.setLayout(l)
l.addWidget(self.canvas)
palette = QtWidgets.QHBoxLayout()
self.add_palette_buttons(palette)
l.addLayout(palette)
self.setCentralWidget(w)
def add_palette_buttons(self, layout):
for c in COLORS:
b = QPaletteButton(c)
b.pressed.connect(lambda c=c: self.canvas.set_pen_color(c))
layout.addWidget(b)
if __name__ == "__main__":
run(MainWindow)
效果:
也可以使用 QWidget
的方式實現(xiàn):按下鼠標左鍵在白色畫布上進行繪制芜赌,實現(xiàn)了簡單的涂鴉板功能仰挣。
from xinet.Qt.qt5 import QtCore, QtGui, QtWidgets
from xinet.run_qt import run
class MainWindow(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("繪圖例子")
self.lastPoint = QtCore.QPoint()
self.endPoint = QtCore.QPoint()
self.initUi()
def initUi(self):
#窗口大小設置為600*500
self.resize(600, 500)
# 畫布大小為400*400,背景為白色
self.pix = QtGui.QPixmap(400, 400)
self.pix.fill(QtCore.Qt.white)
def paintEvent(self, event):
pp = QtGui.QPainter(self.pix)
# 根據(jù)鼠標指針前后兩個位置繪制直線
pp.drawLine(self.lastPoint, self.endPoint)
# 讓前一個坐標值等于后一個坐標值缠沈,
# 這樣就能實現(xiàn)畫出連續(xù)的線
self.lastPoint = self.endPoint
painter = QtGui.QPainter(self)
painter.drawPixmap(0, 0, self.pix)
def mousePressEvent(self, event):
# 鼠標左鍵按下
if event.button() == QtCore.Qt.LeftButton:
self.lastPoint = event.pos()
self.endPoint = self.lastPoint
def mouseMoveEvent(self, event):
# 鼠標左鍵按下的同時移動鼠標
if event.buttons() and QtCore.Qt.LeftButton:
self.endPoint = event.pos()
#進行重新繪制
self.update()
def mouseReleaseEvent(self, event):
# 鼠標左鍵釋放
if event.button() == QtCore.Qt.LeftButton:
self.endPoint = event.pos()
#進行重新繪制
self.update()
if __name__ == '__main__':
run(MainWindow)
效果如下:
2 QPrinter
打印圖像是圖像處理軟件中的一個常用功能膘壶。打印圖像實際上是在 QPaintDevice 中畫圖,與平常在QWidget洲愤、QPixmap 和 QImage 中畫圖一樣颓芭,都是創(chuàng)建一個 QPainter 對象進行畫圖的,只是打印使用的是 QPrinter柬赐,它本質(zhì)上也是一個 QPaintDevice(繪圖設備)亡问。
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QImage, QIcon, QPixmap
from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QSizePolicy, QAction
from PyQt5.QtPrintSupport import QPrinter, QPrintDialog
import sys
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle(self.tr("打印圖片"))
# 創(chuàng)建一個放置圖像的QLabel對象imageLabel,并將該QLabel對象設置為中心窗體肛宋。
self.imageLabel = QLabel()
self.imageLabel.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
self.setCentralWidget(self.imageLabel)
self.image = QImage()
# 創(chuàng)建菜單州藕,工具條等部件
self.createActions()
self.createMenus()
self.createToolBars()
# 在imageLabel對象中放置圖像
if self.image.load("./images/screen.png"):
self.imageLabel.setPixmap(QPixmap.fromImage(self.image))
self.resize(self.image.width(), self.image.height())
def createActions(self):
self.PrintAction = QAction(
QIcon("./images/printer.png"), self.tr("打印"), self)
self.PrintAction.setShortcut("Ctrl+P")
self.PrintAction.setStatusTip(self.tr("打印"))
self.PrintAction.triggered.connect(self.slotPrint)
def createMenus(self):
PrintMenu = self.menuBar().addMenu(self.tr("打印"))
PrintMenu.addAction(self.PrintAction)
def createToolBars(self):
fileToolBar = self.addToolBar("Print")
fileToolBar.addAction(self.PrintAction)
def slotPrint(self):
'''
判斷打印對話框顯示后用戶是否單擊“打印”按鈕,若單擊“打印”按鈕酝陈,
則相關(guān)打印屬性可以通過創(chuàng)建QPrintDialog對象時使用的QPrinter對象獲得床玻,
若用戶單擊“取消”按鈕,則不執(zhí)行后續(xù)的打印操作沉帮。
'''
# 新建一個QPrinter對象
printer = QPrinter()
# 創(chuàng)建一個QPrintDialog對象锈死,參數(shù)為QPrinter對象
printDialog = QPrintDialog(printer, self)
if printDialog.exec_():
# 創(chuàng)建一個QPainter對象,并指定繪圖設備為一個QPrinter對象遇西。
painter = QPainter(printer)
# 獲得QPainter對象的視口矩形
rect = painter.viewport()
# 獲得圖像的大小
size = self.image.size()
# 按照圖形的比例大小重新設置視口矩形
size.scale(rect.size(), Qt.KeepAspectRatio)
painter.setViewport(rect.x(), rect.y(),
size.width(), size.height())
# 設置QPainter窗口大小為圖像的大小
painter.setWindow(self.image.rect())
# 打印
painter.drawImage(0, 0, self.image)
if __name__ == "__main__":
app = QApplication(sys.argv)
main = MainWindow()
main.show()
sys.exit(app.exec_())
效果:
3 雙緩沖繪圖
本節(jié)講解在畫板上繪制矩形馅精,還會講解雙緩沖繪圖的概念。
3.1 繪制矩形粱檀,出現(xiàn)重影
演示繪制矩形的功能洲敢。其完整代碼如下:
from xinet.Qt.qt5 import QtCore, QtGui, QtWidgets
from xinet.run_qt import run
class MainWindow(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("繪制矩形圖形例子")
self.lastPoint = QtCore.QPoint()
self.endPoint = QtCore.QPoint()
self.initUi()
def initUi(self):
# 窗口大小設置為600*500
#self.resize(600, 500)
# 畫布大小為 400*400,背景為白色
self.pix = QtGui.QPixmap(400, 400)
self.pix.fill(QtGui.QColor('white'))
def paintEvent(self, event):
painter = QtGui.QPainter(self)
x = self.lastPoint.x()
y = self.lastPoint.y()
w = self.endPoint.x() - x
h = self.endPoint.y() - y
pp = QtGui.QPainter(self.pix)
pp.drawRect(x, y, w, h)
painter.drawPixmap(0, 0, self.pix)
def mousePressEvent(self, event):
# 鼠標左鍵按下
if event.button() == QtCore.Qt.LeftButton:
self.lastPoint = event.pos()
self.endPoint = self.lastPoint
def mouseMoveEvent(self, event):
# 鼠標左鍵按下的同時移動鼠標
if event.buttons() and QtCore.Qt.LeftButton:
self.endPoint = event.pos()
# 進行重新繪制
self.update()
def mouseReleaseEvent(self, event):
# 鼠標左鍵釋放
if event.button() == QtCore.Qt.LeftButton:
self.endPoint = event.pos()
# 進行重新繪制
self.update()
if __name__ == "__main__":
run(MainWindow)
效果如下:
可以嘗試分別快速和慢速拖動鼠標來繪制矩形茄蚯,結(jié)果發(fā)現(xiàn)压彭,拖動速度越快,重影越少渗常。其實壮不,在拖動鼠標的過程中,屏幕已經(jīng)刷新了很多次皱碘,也可以理解為 paintEvent()
函數(shù)執(zhí)行了多次询一,每執(zhí)行一次就會繪制一個矩形。知道了原因,就可以想辦法來避免出現(xiàn)重影了健蕊。
3.2 使用雙緩沖技術(shù)繪制矩形菱阵,避免出現(xiàn)重影
演示使用雙緩沖技術(shù)繪制矩形,避免出現(xiàn)重影缩功。其完整代碼如下:
from xinet.Qt.qt5 import QtCore, QtGui, QtWidgets
from xinet.run_qt import run
class MainWindow(QtWidgets.QWidget):
def __init__(self, *args, **kw):
super().__init__(*args, **kw)
self.setWindowTitle("雙緩沖繪圖例子")
self.lastPoint = QtCore.QPoint()
self.endPoint = QtCore.QPoint()
# 輔助畫布
self.tempPix = QtGui.QPixmap()
# 標志是否正在繪圖
self.isDrawing = False
self.initUi()
def initUi(self):
# 窗口大小設置為600*500
self.resize(600, 500)
# 畫布大小為400*400晴及,背景為白色
self.pix = QtGui.QPixmap(400, 400)
self.pix.fill(QtCore.Qt.white)
def paintEvent(self, event):
painter = QtGui.QPainter(self)
x = self.lastPoint.x()
y = self.lastPoint.y()
w = self.endPoint.x() - x
h = self.endPoint.y() - y
# 如果正在繪圖,就在輔助畫布上繪制
if self.isDrawing:
# 將以前pix中的內(nèi)容復制到tempPix中嫡锌,保證以前的內(nèi)容不消失
self.tempPix = self.pix
pp = QtGui.QPainter(self.tempPix)
pp.drawRect(x, y, w, h)
painter.drawPixmap(0, 0, self.tempPix)
else:
pp = QtGui.QPainter(self.pix)
pp.drawRect(x, y, w, h)
painter.drawPixmap(0, 0, self.pix)
def mousePressEvent(self, event):
# 鼠標左鍵按下
if event.button() == QtCore.Qt.LeftButton:
self.lastPoint = event.pos()
self.endPoint = self.lastPoint
self.isDrawing = True
def mouseReleaseEvent(self, event):
# 鼠標左鍵釋放
if event.button() == QtCore.Qt.LeftButton:
self.endPoint = event.pos()
# 進行重新繪制
self.update()
self.isDrawing = False
if __name__ == "__main__":
run(MainWindow)
效果如下:
在這個例子中虑稼,按下鼠標左鍵時標志正在繪圖,當釋放鼠標左鍵時則取消正在繪圖的標志势木。運行程序蛛倦,繪圖正常,沒有重影跟压。
在這個例子中胰蝠,需要添加一個輔助畫布,如果正在繪圖震蒋,也就是還沒有釋放鼠標左鍵時茸塞,就在這個輔助畫布上進行;只有釋放鼠標左鍵時查剖,才在真正的畫布上繪圖钾虐。
雙緩沖技術(shù)總結(jié):
在這個例子中,要實現(xiàn)使用鼠標在界面上繪制一個任意大小的矩形而不出現(xiàn)重影笋庄,需要兩個畫布效扫,它們都是 QPixmap 實例,其中 tempPix 作為臨時緩沖區(qū)直砂,當拖動鼠標繪制矩形時菌仁,將內(nèi)容先繪制到 tempPix 上,然后再將 tempPix 繪制到界面上静暂;pix 作為緩沖區(qū)济丘,用來保存已經(jīng)完成的繪制。當釋放鼠標按鍵完成矩形的繪制后洽蛀,則將 tempPix 的內(nèi)容復制到 pix 上摹迷。為了在繪制時不出現(xiàn)重影,而且保證以前繪制的內(nèi)容不消失郊供,那么每一次繪制都是在原來的圖形上進行的峡碉,所以需要在繪制 tempPix 之前,先將 pix 的內(nèi)容復制到 tempPix 上驮审。因為這里有兩個 QPixmap 對象鲫寄,也可以說有兩個緩沖區(qū)吉执,所以稱之為“雙緩沖繪圖”。
4 設置圖片為窗口的背景
import sys
from PyQt5.QtCore import QSize
from PyQt5.QtGui import QImage, QPalette, QBrush, QFont
from PyQt5.QtWidgets import QWidget, QApplication, QLabel
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.setGeometry(100,100,300,200)
oImage = QImage("D:/data/cat.jpg")
sImage = oImage.scaled(QSize(300,200)) # resize Image to widgets size
palette = QPalette()
palette.setBrush(QPalette.Window, QBrush(sImage))
self.setPalette(palette)
self.label = QLabel('測試', self) # test, if it's really backgroundimage
self.label.setFont(QFont("SimSun", 30, QFont.Bold))
self.show()
if __name__ == "__main__":
app = QApplication(sys.argv)
oMainwindow = MainWindow()
sys.exit(app.exec_())
效果: