PyGtk3 缩放 GdkPixbuf 使 Gtk.DrawingArea 绘图变慢
PyGtk3 scaling GdkPixbuf makes Gtk.DrawingArea drawing slower
我有以下层次结构:
滚动窗口
视口
- 绘图区
我实现了一个缩放工具,它基本上可以缩放我在 DrawingArea 中绘制的 GdkPixbuf。最初,图像是 1280x1040。移动卷轴时,Draw 回调函数大约需要 0.005s 来绘制 GdkPixbuf - 看起来非常平滑。
然而,当应用 300% 的缩放级别时,它最多需要 0.03 秒,使其看起来不太平滑。 DrawingArea 的可见部分始终保持不变。好像绘图操作考虑了不可见的区域。
我已经设置了以下代码,所以你们可以 运行 它。缩放比例已经是300%了。
# -*- encoding: utf-8 -*-
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GdkPixbuf
import cairo
import time
class MyWindow(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self, title="DrawingTool")
self.set_default_size(800, 600)
# The Zoom ratio
self.ratio = 3.
# The DrawingImage Brush
self.brush = Brush()
# Image
filename = "image.jpg"
self.original_pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename)
self.displayed_pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename)
self.scale_image()
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
# Zoom buttons
self.button_zoom_in = Gtk.Button(label="Zoom-In")
self.button_zoom_out = Gtk.Button(label="Zoom-Out")
# |ScrolledWindow
# |-> Viewport
# |--> DrawingArea
scrolledwindow = Gtk.ScrolledWindow()
viewport = Gtk.Viewport()
self.drawing_area = Gtk.DrawingArea()
self.drawing_area.set_size_request(
self.displayed_pixbuf.get_width(), self.displayed_pixbuf.get_height())
self.drawing_area.set_events(Gdk.EventMask.ALL_EVENTS_MASK)
# Pack
viewport.add(self.drawing_area)
scrolledwindow.add(viewport)
box.pack_start(self.button_zoom_in, False, True, 0)
box.pack_start(self.button_zoom_out, False, True, 0)
box.pack_start(scrolledwindow, True, True, 0)
self.add(box)
# Connect
self.connect("destroy", Gtk.main_quit)
self.button_zoom_in.connect("clicked", self.on_button_zoom_in_clicked)
self.button_zoom_out.connect("clicked", self.on_button_zoom_out_clicked)
self.drawing_area.connect("enter-notify-event", self.on_drawing_area_mouse_enter)
self.drawing_area.connect("leave-notify-event", self.on_drawing_area_mouse_leave)
self.drawing_area.connect("motion-notify-event", self.on_drawing_area_mouse_motion)
self.drawing_area.connect("draw", self.on_drawing_area_draw)
self.drawing_area.connect("button-press-event", self.on_drawing_area_button_press_event)
self.drawing_area.connect("button-release-event", self.on_drawing_area_button_release_event)
self.show_all()
def on_button_zoom_in_clicked(self, widget):
self.ratio += 0.1
self.scale_image()
self.drawing_area.queue_draw()
def on_button_zoom_out_clicked(self, widget):
self.ratio -= 0.1
self.scale_image()
self.drawing_area.queue_draw()
def scale_image(self):
self.displayed_pixbuf = self.original_pixbuf.scale_simple(self.original_pixbuf.get_width() * self.ratio,
self.original_pixbuf.get_height() * self.ratio, 2)
def on_drawing_area_draw(self, drawable, cairo_context):
start = time.time()
# DrawingArea size depends on Pixbuf size
self.drawing_area.get_window().resize(self.displayed_pixbuf.get_width(),
self.displayed_pixbuf .get_height())
self.drawing_area.set_size_request(self.displayed_pixbuf.get_width(),
self.displayed_pixbuf.get_height())
# Draw image
Gdk.cairo_set_source_pixbuf(cairo_context, self.displayed_pixbuf, 0, 0)
cairo_context.paint()
# Draw lines
self.brush._draw(cairo_context)
end = time.time()
print(f"Runtime of the program is {end - start}")
def on_drawing_area_mouse_enter(self, widget, event):
print("In - DrawingArea")
def on_drawing_area_mouse_leave(self, widget, event):
print("Out - DrawingArea")
def on_drawing_area_mouse_motion(self, widget, event):
(x, y) = int(event.x), int(event.y)
# Should not happen but just in case.
if not ( (x >= 0 and x < self.displayed_pixbuf.get_width()) and
(y >= 0 and y < self.displayed_pixbuf.get_height()) ):
return True
# If user is holding the left mouse button
if event.state & Gdk.EventMask.BUTTON_PRESS_MASK:
self.brush._add_point((x, y))
self.drawing_area.queue_draw()
def on_drawing_area_button_press_event(self, widget, event):
self.brush._add_point((int(event.x), int(event.y)))
def on_drawing_area_button_release_event(self, widget, event):
self.brush._line_ended()
# ## ## ## ## ## ## ## ## ## ## ## ## # ## ## ## ## ## ## ## ## ## ## ## ## #
# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ #
# #
# Brush : #
# #
# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ #
## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
class Brush(object):
default_rgba_color = (0, 0, 0, 1)
def __init__(self, width=None, rgba_color=None):
if rgba_color is None:
rgba_color = self.default_rgba_color
if width is None:
width = 3
self.__width = width
self.__rgba_color = rgba_color
self.__stroke = []
self.__current_line = []
def _line_ended(self):
self.__stroke.append(self.__current_line.copy())
self.__current_line = []
def _add_point(self, point):
self.__current_line.append(point)
def _draw(self, cairo_context):
cairo_context.set_source_rgba(*self.__rgba_color)
cairo_context.set_line_width(self.__width)
cairo_context.set_line_cap(cairo.LINE_CAP_ROUND)
cairo_context.new_path()
for line in self.__stroke:
for x, y in line:
cairo_context.line_to(x, y)
cairo_context.new_sub_path()
for x, y in self.__current_line:
cairo_context.line_to(x, y)
cairo_context.stroke()
# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ #
# ~ Getters & Setters ~ #
# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ #
def _get_width(self):
return self.__width
def _set_width(self, width):
self.__width = width
def _get_rgba_color(self):
return self.__rgba_color
def _set_rgba_color(self, rgba_color):
self.__rgba_color = rgba_color
def _get_stroke(self):
return self.__stroke
def _get_current_line(self):
return self.__current_line
MyWindow()
Gtk.main()
那么,这是正常的不可避免的事情吗?
编辑
这是实现的解决方案的完整代码。
# -*- encoding: utf-8 -*-
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GdkPixbuf
import cairo
import time
class MyWindow(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self, title="DrawingTool")
self.set_default_size(800, 600)
# The Zoom ratio
self.ratio = 3.
# The DrawingImage Brush
self.brush = Brush()
# Image
filename = "image.jpg"
self.original_pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename)
self.displayed_pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename)
self.scale_image()
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
# Zoom buttons
self.button_zoom_in = Gtk.Button(label="Zoom-In")
self.button_zoom_out = Gtk.Button(label="Zoom-Out")
# |ScrolledWindow
# |-> Viewport
# |--> DrawingArea
scrolledwindow = Gtk.ScrolledWindow()
self.viewport = Gtk.Viewport()
self.drawing_area = Gtk.DrawingArea()
self.drawing_area.set_size_request(
self.displayed_pixbuf.get_width(), self.displayed_pixbuf.get_height())
self.drawing_area.set_events(Gdk.EventMask.ALL_EVENTS_MASK)
# Pack
self.viewport.add(self.drawing_area)
scrolledwindow.add(self.viewport)
box.pack_start(self.button_zoom_in, False, True, 0)
box.pack_start(self.button_zoom_out, False, True, 0)
box.pack_start(scrolledwindow, True, True, 0)
self.add(box)
# Connect
self.connect("destroy", Gtk.main_quit)
self.button_zoom_in.connect("clicked", self.on_button_zoom_in_clicked)
self.button_zoom_out.connect("clicked", self.on_button_zoom_out_clicked)
self.drawing_area.connect("enter-notify-event", self.on_drawing_area_mouse_enter)
self.drawing_area.connect("leave-notify-event", self.on_drawing_area_mouse_leave)
self.drawing_area.connect("motion-notify-event", self.on_drawing_area_mouse_motion)
self.drawing_area.connect("draw", self.on_drawing_area_draw)
self.drawing_area.connect("button-press-event", self.on_drawing_area_button_press_event)
self.drawing_area.connect("button-release-event", self.on_drawing_area_button_release_event)
scrolledwindow.get_hscrollbar().connect("value-changed", self.on_scrolledwindow_horizontal_scrollbar_value_changed)
scrolledwindow.get_vscrollbar().connect("value-changed", self.on_scrolledwindow_vertical_scrollbar_value_changed)
self.show_all()
def on_button_zoom_in_clicked(self, widget):
self.ratio += 0.1
self.scale_image()
self.drawing_area.queue_draw()
def on_button_zoom_out_clicked(self, widget):
self.ratio -= 0.1
self.scale_image()
self.drawing_area.queue_draw()
def scale_image(self):
self.displayed_pixbuf = self.original_pixbuf.scale_simple(self.original_pixbuf.get_width() * self.ratio,
self.original_pixbuf.get_height() * self.ratio, 2)
def on_scrolledwindow_horizontal_scrollbar_value_changed(self, scrollbar):
self.drawing_area.queue_draw()
def on_scrolledwindow_vertical_scrollbar_value_changed(self, scrollbar):
self.drawing_area.queue_draw()
def on_drawing_area_draw(self, drawable, cairo_context):
start = time.time()
# DrawingArea size depends on Pixbuf size
self.drawing_area.get_window().resize(self.displayed_pixbuf.get_width(),
self.displayed_pixbuf .get_height())
self.drawing_area.set_size_request(self.displayed_pixbuf.get_width(),
self.displayed_pixbuf.get_height())
# (x, y) offsets
pixbuf_x = int(self.viewport.get_hadjustment().get_value())
pixbuf_y = int(self.viewport.get_vadjustment().get_value())
# Width and height of the image's clip
width = cairo_context.get_target().get_width()
height = cairo_context.get_target().get_height()
if pixbuf_x + width > self.displayed_pixbuf.get_width():
width = self.displayed_pixbuf.get_width() - pixbuf_x
if pixbuf_y + height > self.displayed_pixbuf.get_height():
height = self.displayed_pixbuf.get_height() - pixbuf_y
if width > 0 and height > 0:
# Create the area of the image that will be displayed in the right position
image = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, False, 8, width, height)
self.displayed_pixbuf.copy_area(pixbuf_x, pixbuf_y, width, height, image, 0, 0)
# Draw created area of the Sample's Pixbuf
Gdk.cairo_set_source_pixbuf(cairo_context, image, pixbuf_x, pixbuf_y)
cairo_context.paint()
# Draw brush strokes
self.brush._draw(cairo_context)
end = time.time()
print(f"Runtime of the program is {end - start}")
def on_drawing_area_mouse_enter(self, widget, event):
print("In - DrawingArea")
def on_drawing_area_mouse_leave(self, widget, event):
print("Out - DrawingArea")
def on_drawing_area_mouse_motion(self, widget, event):
(x, y) = int(event.x), int(event.y)
# Should not happen but just in case.
if not ( (x >= 0 and x < self.displayed_pixbuf.get_width()) and
(y >= 0 and y < self.displayed_pixbuf.get_height()) ):
return True
# If user is holding the left mouse button
if event.state & Gdk.EventMask.BUTTON_PRESS_MASK:
self.brush._add_point((x, y))
self.drawing_area.queue_draw()
def on_drawing_area_button_press_event(self, widget, event):
self.brush._add_point((int(event.x), int(event.y)))
def on_drawing_area_button_release_event(self, widget, event):
self.brush._line_ended()
# ## ## ## ## ## ## ## ## ## ## ## ## # ## ## ## ## ## ## ## ## ## ## ## ## #
# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ #
# #
# Brush : #
# #
# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ #
## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
class Brush(object):
default_rgba_color = (0, 0, 0, 1)
def __init__(self, width=None, rgba_color=None):
if rgba_color is None:
rgba_color = self.default_rgba_color
if width is None:
width = 3
self.__width = width
self.__rgba_color = rgba_color
self.__stroke = []
self.__current_line = []
def _line_ended(self):
self.__stroke.append(self.__current_line.copy())
self.__current_line = []
def _add_point(self, point):
self.__current_line.append(point)
def _draw(self, cairo_context):
cairo_context.set_source_rgba(*self.__rgba_color)
cairo_context.set_line_width(self.__width)
cairo_context.set_line_cap(cairo.LINE_CAP_ROUND)
cairo_context.new_path()
for line in self.__stroke:
for x, y in line:
cairo_context.line_to(x, y)
cairo_context.new_sub_path()
for x, y in self.__current_line:
cairo_context.line_to(x, y)
cairo_context.stroke()
# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ #
# ~ Getters & Setters ~ #
# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ #
def _get_width(self):
return self.__width
def _set_width(self, width):
self.__width = width
def _get_rgba_color(self):
return self.__rgba_color
def _set_rgba_color(self, rgba_color):
self.__rgba_color = rgba_color
def _get_stroke(self):
return self.__stroke
def _get_current_line(self):
return self.__current_line
MyWindow()
Gtk.main()
我已经想出如何解决这个效率问题了。我所做的是,我现在不绘制整个图像,而是绘制需要重新绘制的图像的特定区域。
我会在代码下面解释每一行:
''' Draw method. '''
def _draw(self, cairo_context, pixbuf):
# Set drawing area size
self.__drawing_area.get_window().resize(pixbuf.get_width(), pixbuf.get_height())
self.__drawing_area.set_size_request(pixbuf.get_width(), pixbuf.get_height())
# (x, y) offsets
pixbuf_x = int(self.__viewport.get_hadjustment().get_value())
pixbuf_y = int(self.__viewport.get_vadjustment().get_value())
# Width and height of the image's clip
width = cairo_context.get_target().get_width()
height = cairo_context.get_target().get_height()
if pixbuf_x + width > pixbuf.get_width():
width = pixbuf.get_width() - pixbuf_x
if pixbuf_y + height > pixbuf.get_height():
height = pixbuf.get_height() - pixbuf_y
if width > 0 and height > 0:
# Create the area of the image that will be displayed in the right position
image = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, False, 8, width, height)
pixbuf.copy_area(pixbuf_x, pixbuf_y, width, height, image, 0, 0)
# Draw created area of the Sample's Pixbuf
Gdk.cairo_set_source_pixbuf(cairo_context, image, pixbuf_x, pixbuf_y)
cairo_context.paint()
# Draw brush strokes
self.__brush._draw(cairo_context)
这些行设置 Gtk.DrawingArea 大小。当我的 Pixbuf 大小大于 Gtk.DrawingArea 可见区域时,Gtk.ScrolledWindow 的滚动条知道这一点并允许您沿着图像移动。当您绘制的 Pixbuf 小于 Gtk.DrawingArea 可见区域时需要第一行,因此例如当您将鼠标信号连接到 Gtk.DrawingArea 时,这些信号仅在鼠标悬停时发出图片。
# Set drawing area size
self.__drawing_area.get_window().resize(pixbuf.get_width(), pixbuf.get_height())
self.__drawing_area.set_size_request(pixbuf.get_width(), pixbuf.get_height())
x 和 y 偏移量是您需要的左侧和顶部像素:
i) 告诉 cairo 在 Gtk.DrawingArea
中画画的地方
ii) 剪辑你的图片
我正在使用 Gtk.Viewport,但您也可以使用 Gtk.ScrolledWindow.get_hadjustment().get_value()
和 Gtk.ScrolledWindow.get_vadjustment().get_value()
,例如。
# (x, y) offsets
pixbuf_x = int(self.__viewport.get_hadjustment().get_value())
pixbuf_y = int(self.__viewport.get_vadjustment().get_value())
在接下来的几行中,我只计算了剪裁图像所需的宽度和高度。这是根据 Gtk.DrawingArea 可见区域的大小和您的图像大小完成的。使用 cairo_context.get_target().get_width()
你基本上得到了 Gtk.DrawingArea 可见区域的宽度,反之亦然。
# Width and height of the image's clip
width = cairo_context.get_target().get_width()
height = cairo_context.get_target().get_height()
if pixbuf_x + width > pixbuf.get_width():
width = pixbuf.get_width() - pixbuf_x
if pixbuf_y + height > pixbuf.get_height():
height = pixbuf.get_height() - pixbuf_y
最后你只需要裁剪你的原始图像并将其绘制在 Gtk.DrawingArea 的正确位置。 if-then-else 只是我在右下边缘缩小时克服问题的一种解决方法,因为 Gtk 组件 return 获取偏移量的值似乎不会在它们时更新需要。
编辑
我忘了说当滚动条移动时你还需要重新绘制图像。否则会渲染垃圾。请参阅原始问题编辑部分的完整代码中的最后 2 connect
个方法。
我有以下层次结构:
滚动窗口
视口
- 绘图区
我实现了一个缩放工具,它基本上可以缩放我在 DrawingArea 中绘制的 GdkPixbuf。最初,图像是 1280x1040。移动卷轴时,Draw 回调函数大约需要 0.005s 来绘制 GdkPixbuf - 看起来非常平滑。
然而,当应用 300% 的缩放级别时,它最多需要 0.03 秒,使其看起来不太平滑。 DrawingArea 的可见部分始终保持不变。好像绘图操作考虑了不可见的区域。
我已经设置了以下代码,所以你们可以 运行 它。缩放比例已经是300%了。
# -*- encoding: utf-8 -*-
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GdkPixbuf
import cairo
import time
class MyWindow(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self, title="DrawingTool")
self.set_default_size(800, 600)
# The Zoom ratio
self.ratio = 3.
# The DrawingImage Brush
self.brush = Brush()
# Image
filename = "image.jpg"
self.original_pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename)
self.displayed_pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename)
self.scale_image()
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
# Zoom buttons
self.button_zoom_in = Gtk.Button(label="Zoom-In")
self.button_zoom_out = Gtk.Button(label="Zoom-Out")
# |ScrolledWindow
# |-> Viewport
# |--> DrawingArea
scrolledwindow = Gtk.ScrolledWindow()
viewport = Gtk.Viewport()
self.drawing_area = Gtk.DrawingArea()
self.drawing_area.set_size_request(
self.displayed_pixbuf.get_width(), self.displayed_pixbuf.get_height())
self.drawing_area.set_events(Gdk.EventMask.ALL_EVENTS_MASK)
# Pack
viewport.add(self.drawing_area)
scrolledwindow.add(viewport)
box.pack_start(self.button_zoom_in, False, True, 0)
box.pack_start(self.button_zoom_out, False, True, 0)
box.pack_start(scrolledwindow, True, True, 0)
self.add(box)
# Connect
self.connect("destroy", Gtk.main_quit)
self.button_zoom_in.connect("clicked", self.on_button_zoom_in_clicked)
self.button_zoom_out.connect("clicked", self.on_button_zoom_out_clicked)
self.drawing_area.connect("enter-notify-event", self.on_drawing_area_mouse_enter)
self.drawing_area.connect("leave-notify-event", self.on_drawing_area_mouse_leave)
self.drawing_area.connect("motion-notify-event", self.on_drawing_area_mouse_motion)
self.drawing_area.connect("draw", self.on_drawing_area_draw)
self.drawing_area.connect("button-press-event", self.on_drawing_area_button_press_event)
self.drawing_area.connect("button-release-event", self.on_drawing_area_button_release_event)
self.show_all()
def on_button_zoom_in_clicked(self, widget):
self.ratio += 0.1
self.scale_image()
self.drawing_area.queue_draw()
def on_button_zoom_out_clicked(self, widget):
self.ratio -= 0.1
self.scale_image()
self.drawing_area.queue_draw()
def scale_image(self):
self.displayed_pixbuf = self.original_pixbuf.scale_simple(self.original_pixbuf.get_width() * self.ratio,
self.original_pixbuf.get_height() * self.ratio, 2)
def on_drawing_area_draw(self, drawable, cairo_context):
start = time.time()
# DrawingArea size depends on Pixbuf size
self.drawing_area.get_window().resize(self.displayed_pixbuf.get_width(),
self.displayed_pixbuf .get_height())
self.drawing_area.set_size_request(self.displayed_pixbuf.get_width(),
self.displayed_pixbuf.get_height())
# Draw image
Gdk.cairo_set_source_pixbuf(cairo_context, self.displayed_pixbuf, 0, 0)
cairo_context.paint()
# Draw lines
self.brush._draw(cairo_context)
end = time.time()
print(f"Runtime of the program is {end - start}")
def on_drawing_area_mouse_enter(self, widget, event):
print("In - DrawingArea")
def on_drawing_area_mouse_leave(self, widget, event):
print("Out - DrawingArea")
def on_drawing_area_mouse_motion(self, widget, event):
(x, y) = int(event.x), int(event.y)
# Should not happen but just in case.
if not ( (x >= 0 and x < self.displayed_pixbuf.get_width()) and
(y >= 0 and y < self.displayed_pixbuf.get_height()) ):
return True
# If user is holding the left mouse button
if event.state & Gdk.EventMask.BUTTON_PRESS_MASK:
self.brush._add_point((x, y))
self.drawing_area.queue_draw()
def on_drawing_area_button_press_event(self, widget, event):
self.brush._add_point((int(event.x), int(event.y)))
def on_drawing_area_button_release_event(self, widget, event):
self.brush._line_ended()
# ## ## ## ## ## ## ## ## ## ## ## ## # ## ## ## ## ## ## ## ## ## ## ## ## #
# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ #
# #
# Brush : #
# #
# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ #
## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
class Brush(object):
default_rgba_color = (0, 0, 0, 1)
def __init__(self, width=None, rgba_color=None):
if rgba_color is None:
rgba_color = self.default_rgba_color
if width is None:
width = 3
self.__width = width
self.__rgba_color = rgba_color
self.__stroke = []
self.__current_line = []
def _line_ended(self):
self.__stroke.append(self.__current_line.copy())
self.__current_line = []
def _add_point(self, point):
self.__current_line.append(point)
def _draw(self, cairo_context):
cairo_context.set_source_rgba(*self.__rgba_color)
cairo_context.set_line_width(self.__width)
cairo_context.set_line_cap(cairo.LINE_CAP_ROUND)
cairo_context.new_path()
for line in self.__stroke:
for x, y in line:
cairo_context.line_to(x, y)
cairo_context.new_sub_path()
for x, y in self.__current_line:
cairo_context.line_to(x, y)
cairo_context.stroke()
# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ #
# ~ Getters & Setters ~ #
# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ #
def _get_width(self):
return self.__width
def _set_width(self, width):
self.__width = width
def _get_rgba_color(self):
return self.__rgba_color
def _set_rgba_color(self, rgba_color):
self.__rgba_color = rgba_color
def _get_stroke(self):
return self.__stroke
def _get_current_line(self):
return self.__current_line
MyWindow()
Gtk.main()
那么,这是正常的不可避免的事情吗?
编辑
这是实现的解决方案的完整代码。
# -*- encoding: utf-8 -*-
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GdkPixbuf
import cairo
import time
class MyWindow(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self, title="DrawingTool")
self.set_default_size(800, 600)
# The Zoom ratio
self.ratio = 3.
# The DrawingImage Brush
self.brush = Brush()
# Image
filename = "image.jpg"
self.original_pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename)
self.displayed_pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename)
self.scale_image()
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
# Zoom buttons
self.button_zoom_in = Gtk.Button(label="Zoom-In")
self.button_zoom_out = Gtk.Button(label="Zoom-Out")
# |ScrolledWindow
# |-> Viewport
# |--> DrawingArea
scrolledwindow = Gtk.ScrolledWindow()
self.viewport = Gtk.Viewport()
self.drawing_area = Gtk.DrawingArea()
self.drawing_area.set_size_request(
self.displayed_pixbuf.get_width(), self.displayed_pixbuf.get_height())
self.drawing_area.set_events(Gdk.EventMask.ALL_EVENTS_MASK)
# Pack
self.viewport.add(self.drawing_area)
scrolledwindow.add(self.viewport)
box.pack_start(self.button_zoom_in, False, True, 0)
box.pack_start(self.button_zoom_out, False, True, 0)
box.pack_start(scrolledwindow, True, True, 0)
self.add(box)
# Connect
self.connect("destroy", Gtk.main_quit)
self.button_zoom_in.connect("clicked", self.on_button_zoom_in_clicked)
self.button_zoom_out.connect("clicked", self.on_button_zoom_out_clicked)
self.drawing_area.connect("enter-notify-event", self.on_drawing_area_mouse_enter)
self.drawing_area.connect("leave-notify-event", self.on_drawing_area_mouse_leave)
self.drawing_area.connect("motion-notify-event", self.on_drawing_area_mouse_motion)
self.drawing_area.connect("draw", self.on_drawing_area_draw)
self.drawing_area.connect("button-press-event", self.on_drawing_area_button_press_event)
self.drawing_area.connect("button-release-event", self.on_drawing_area_button_release_event)
scrolledwindow.get_hscrollbar().connect("value-changed", self.on_scrolledwindow_horizontal_scrollbar_value_changed)
scrolledwindow.get_vscrollbar().connect("value-changed", self.on_scrolledwindow_vertical_scrollbar_value_changed)
self.show_all()
def on_button_zoom_in_clicked(self, widget):
self.ratio += 0.1
self.scale_image()
self.drawing_area.queue_draw()
def on_button_zoom_out_clicked(self, widget):
self.ratio -= 0.1
self.scale_image()
self.drawing_area.queue_draw()
def scale_image(self):
self.displayed_pixbuf = self.original_pixbuf.scale_simple(self.original_pixbuf.get_width() * self.ratio,
self.original_pixbuf.get_height() * self.ratio, 2)
def on_scrolledwindow_horizontal_scrollbar_value_changed(self, scrollbar):
self.drawing_area.queue_draw()
def on_scrolledwindow_vertical_scrollbar_value_changed(self, scrollbar):
self.drawing_area.queue_draw()
def on_drawing_area_draw(self, drawable, cairo_context):
start = time.time()
# DrawingArea size depends on Pixbuf size
self.drawing_area.get_window().resize(self.displayed_pixbuf.get_width(),
self.displayed_pixbuf .get_height())
self.drawing_area.set_size_request(self.displayed_pixbuf.get_width(),
self.displayed_pixbuf.get_height())
# (x, y) offsets
pixbuf_x = int(self.viewport.get_hadjustment().get_value())
pixbuf_y = int(self.viewport.get_vadjustment().get_value())
# Width and height of the image's clip
width = cairo_context.get_target().get_width()
height = cairo_context.get_target().get_height()
if pixbuf_x + width > self.displayed_pixbuf.get_width():
width = self.displayed_pixbuf.get_width() - pixbuf_x
if pixbuf_y + height > self.displayed_pixbuf.get_height():
height = self.displayed_pixbuf.get_height() - pixbuf_y
if width > 0 and height > 0:
# Create the area of the image that will be displayed in the right position
image = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, False, 8, width, height)
self.displayed_pixbuf.copy_area(pixbuf_x, pixbuf_y, width, height, image, 0, 0)
# Draw created area of the Sample's Pixbuf
Gdk.cairo_set_source_pixbuf(cairo_context, image, pixbuf_x, pixbuf_y)
cairo_context.paint()
# Draw brush strokes
self.brush._draw(cairo_context)
end = time.time()
print(f"Runtime of the program is {end - start}")
def on_drawing_area_mouse_enter(self, widget, event):
print("In - DrawingArea")
def on_drawing_area_mouse_leave(self, widget, event):
print("Out - DrawingArea")
def on_drawing_area_mouse_motion(self, widget, event):
(x, y) = int(event.x), int(event.y)
# Should not happen but just in case.
if not ( (x >= 0 and x < self.displayed_pixbuf.get_width()) and
(y >= 0 and y < self.displayed_pixbuf.get_height()) ):
return True
# If user is holding the left mouse button
if event.state & Gdk.EventMask.BUTTON_PRESS_MASK:
self.brush._add_point((x, y))
self.drawing_area.queue_draw()
def on_drawing_area_button_press_event(self, widget, event):
self.brush._add_point((int(event.x), int(event.y)))
def on_drawing_area_button_release_event(self, widget, event):
self.brush._line_ended()
# ## ## ## ## ## ## ## ## ## ## ## ## # ## ## ## ## ## ## ## ## ## ## ## ## #
# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ #
# #
# Brush : #
# #
# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ #
## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
class Brush(object):
default_rgba_color = (0, 0, 0, 1)
def __init__(self, width=None, rgba_color=None):
if rgba_color is None:
rgba_color = self.default_rgba_color
if width is None:
width = 3
self.__width = width
self.__rgba_color = rgba_color
self.__stroke = []
self.__current_line = []
def _line_ended(self):
self.__stroke.append(self.__current_line.copy())
self.__current_line = []
def _add_point(self, point):
self.__current_line.append(point)
def _draw(self, cairo_context):
cairo_context.set_source_rgba(*self.__rgba_color)
cairo_context.set_line_width(self.__width)
cairo_context.set_line_cap(cairo.LINE_CAP_ROUND)
cairo_context.new_path()
for line in self.__stroke:
for x, y in line:
cairo_context.line_to(x, y)
cairo_context.new_sub_path()
for x, y in self.__current_line:
cairo_context.line_to(x, y)
cairo_context.stroke()
# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ #
# ~ Getters & Setters ~ #
# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ #
def _get_width(self):
return self.__width
def _set_width(self, width):
self.__width = width
def _get_rgba_color(self):
return self.__rgba_color
def _set_rgba_color(self, rgba_color):
self.__rgba_color = rgba_color
def _get_stroke(self):
return self.__stroke
def _get_current_line(self):
return self.__current_line
MyWindow()
Gtk.main()
我已经想出如何解决这个效率问题了。我所做的是,我现在不绘制整个图像,而是绘制需要重新绘制的图像的特定区域。
我会在代码下面解释每一行:
''' Draw method. '''
def _draw(self, cairo_context, pixbuf):
# Set drawing area size
self.__drawing_area.get_window().resize(pixbuf.get_width(), pixbuf.get_height())
self.__drawing_area.set_size_request(pixbuf.get_width(), pixbuf.get_height())
# (x, y) offsets
pixbuf_x = int(self.__viewport.get_hadjustment().get_value())
pixbuf_y = int(self.__viewport.get_vadjustment().get_value())
# Width and height of the image's clip
width = cairo_context.get_target().get_width()
height = cairo_context.get_target().get_height()
if pixbuf_x + width > pixbuf.get_width():
width = pixbuf.get_width() - pixbuf_x
if pixbuf_y + height > pixbuf.get_height():
height = pixbuf.get_height() - pixbuf_y
if width > 0 and height > 0:
# Create the area of the image that will be displayed in the right position
image = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, False, 8, width, height)
pixbuf.copy_area(pixbuf_x, pixbuf_y, width, height, image, 0, 0)
# Draw created area of the Sample's Pixbuf
Gdk.cairo_set_source_pixbuf(cairo_context, image, pixbuf_x, pixbuf_y)
cairo_context.paint()
# Draw brush strokes
self.__brush._draw(cairo_context)
这些行设置 Gtk.DrawingArea 大小。当我的 Pixbuf 大小大于 Gtk.DrawingArea 可见区域时,Gtk.ScrolledWindow 的滚动条知道这一点并允许您沿着图像移动。当您绘制的 Pixbuf 小于 Gtk.DrawingArea 可见区域时需要第一行,因此例如当您将鼠标信号连接到 Gtk.DrawingArea 时,这些信号仅在鼠标悬停时发出图片。
# Set drawing area size
self.__drawing_area.get_window().resize(pixbuf.get_width(), pixbuf.get_height())
self.__drawing_area.set_size_request(pixbuf.get_width(), pixbuf.get_height())
x 和 y 偏移量是您需要的左侧和顶部像素:
i) 告诉 cairo 在 Gtk.DrawingArea
中画画的地方ii) 剪辑你的图片
我正在使用 Gtk.Viewport,但您也可以使用 Gtk.ScrolledWindow.get_hadjustment().get_value()
和 Gtk.ScrolledWindow.get_vadjustment().get_value()
,例如。
# (x, y) offsets
pixbuf_x = int(self.__viewport.get_hadjustment().get_value())
pixbuf_y = int(self.__viewport.get_vadjustment().get_value())
在接下来的几行中,我只计算了剪裁图像所需的宽度和高度。这是根据 Gtk.DrawingArea 可见区域的大小和您的图像大小完成的。使用 cairo_context.get_target().get_width()
你基本上得到了 Gtk.DrawingArea 可见区域的宽度,反之亦然。
# Width and height of the image's clip
width = cairo_context.get_target().get_width()
height = cairo_context.get_target().get_height()
if pixbuf_x + width > pixbuf.get_width():
width = pixbuf.get_width() - pixbuf_x
if pixbuf_y + height > pixbuf.get_height():
height = pixbuf.get_height() - pixbuf_y
最后你只需要裁剪你的原始图像并将其绘制在 Gtk.DrawingArea 的正确位置。 if-then-else 只是我在右下边缘缩小时克服问题的一种解决方法,因为 Gtk 组件 return 获取偏移量的值似乎不会在它们时更新需要。
编辑
我忘了说当滚动条移动时你还需要重新绘制图像。否则会渲染垃圾。请参阅原始问题编辑部分的完整代码中的最后 2 connect
个方法。