PyGtk3缩放GdkPixbuf使Gtk.DrawingArea绘制速度变慢

时间:2019-11-08 01:07:44

标签: python-3.x gtk3 pygtk pycairo drawingarea

我具有以下层次结构:

  • ScrolledWindow

    • ViewPort

      • DrawingArea

我已经实现了一个缩放工具,该工具基本上可以缩放我在DrawingArea中绘制的GdkPixbuf。原始图片为1280x1040。移动滚动时,Draw回调函数大约需要0.005秒的时间来绘制GdkPixbuf-看起来非常平滑。

但是,当应用300%的缩放级别时,它最多需要0.03s的时间,使其看起来不那么平滑。 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()

1 个答案:

答案 0 :(得分:1)

我已经弄清楚了如何解决效率问题。我要做的是,我现在不绘制整个图像,而是绘制需要重绘的图像特定区域。

我将在代码下方解释每一行:

    ''' 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时,仅当鼠标悬停在图像上方时才会发出这些信号。 / p>

# 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)告诉开罗在哪里绘制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组件返回以获取偏移量的值似乎在需要时不会更新。 / p>

编辑

我忘了提一下,滚动条移动时还需要重新绘制图像。否则将产生垃圾。请参阅原始问题的编辑部分中完整代码中的最后2个connect方法。