GDKPixbuf при масштабировании PyGtk3 замедляет рисование Gtk.DrawingArea - PullRequest
1 голос
/ 08 ноября 2019

У меня есть следующая иерархия:

  • ScrolledWindow

    • ViewPort

      • DrawingArea

Я реализовал инструмент Zoom, который в основном масштабирует GdkPixbuf, который я рисую в своей DrawingArea. Первоначально изображение 1280x1040. При перемещении прокрутки функция обратного вызова Draw отрисовывает GdkPixbuf примерно 0,005 с - она ​​выглядит действительно гладкой.

Однако при применении уровня масштабирования 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()

1 Ответ

1 голос
/ 14 ноября 2019

Я понял, как решить эту проблему эффективности. Я сделал вместо того, чтобы рисовать все изображение, теперь я рисую определенную область изображения, которую необходимо перерисовать.

Я объясню каждую строку под кодом:

    ''' 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) Скажите Каиру, где рисовать в 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 для получения смещений, по-видимому, не обновляются, когда это необходимо.

РЕДАКТИРОВАТЬ

Я забыл упомянуть, что вам также необходимо перерисовать изображение при перемещении полос прокрутки. В противном случае мусор будет оказан. См. Последние 2 connect методов в полном коде в отредактированной части исходного вопроса.

...