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 个方法。