I'm trying to write simple graphic editor using PyGObject and python 3. I need to draw lines with different color and width using mouse. I found many examples like this but nothing more complex.
How do I save drawn image between 'draw' events? Is there incremental way of drawing or do I have to redraw pane on each 'draw' event? I found out that I can save path but how can I save width and colors of drawn lines? Is there way create image outside 'draw' callback and only apply (draw) it inside callback?
Here is what I have for now.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from gi.repository import Gtk, Gdk
import os
class App(object):
main_ui = os.path.join(os.path.dirname(__file__), 'gui.glade')
def __init__(self):
self.builder = Gtk.Builder()
self.builder.add_from_file(self.main_ui)
self.main_window.connect('destroy', self.quit)
self.mw_quit_button.connect('clicked', self.quit)
self.mw_graph_editor_button.connect('clicked', self.show_window, self.graph_editor_window)
self.graph_editor_window.connect('delete-event', self.hide_window_delete)
self.ge_menubar_file_quit.connect('activate', self.hide_window, self.graph_editor_window)
self.ge_toolbar_quit.connect('clicked', self.hide_window, self.graph_editor_window)
self.ge_drawingarea.connect('motion-notify-event', self.pointer_motion)
self.ge_drawingarea.connect('motion-notify-event', self.show_coordinates)
self.ge_drawingarea.connect('draw', self.draw_callback)
self.path = None
self.coord = (0, 0)
self.rgb = (0, 0, 0)
def __getattr__(self, name):
obj = self.builder.get_object(name)
if not obj:
raise AttributeError("Object {0} has no attribute {1}".format(self, name))
setattr(self, name, obj)
return obj
def draw_callback(self, drawingarea, cr):
if self.path:
cr.append_path(self.path)
cr.line_to(self.coord[0], self.coord[1])
cr.set_source_rgba(*self.rgb)
self.path = cr.copy_path_flat()
cr.stroke()
def show_coordinates(self, window, event):
self.ge_mouse_coordinates.set_label('X: {0:.0f} Y: {1:.0f}'.format(event.x, event.y))
def pointer_motion(self, widget, event):
if event.state & Gdk.ModifierType.BUTTON1_MASK:
self.draw(widget, event.x, event.y)
elif event.state & Gdk.ModifierType.BUTTON3_MASK:
self.draw(widget, event.x, event.y, True)
def draw(self, widget, x, y, erase=False):
self.coord = (x,y)
if erase:
self.rgb = (256, 256, 256)
else:
self.rgb = (0, 0, 0)
widget.queue_draw()
def show_window(self, widget, data):
data.show_all()
def hide_window_delete(self, widget, event):
widget.hide()
return True
def hide_window(self, widget, window):
window.hide()
def run(self):
self.main_window.show_all()
Gtk.main()
def quit(self, widget=None, data=None):
self.main_window.destroy()
Gtk.main_quit()
if __name__ == "__main__":
app = App()
app.run()
Sorry for my English, it is not my native language.
You need to use a Double Buffer technique:
http://en.wikipedia.org/wiki/Multiple_buffering#Double_buffering_in_computer_graphics
That is you have an image, and you draw over that image: that image is the "behind the scenes" buffer. You can have a lot of methods that draw something to that image. Then, on the callback that responds to 'draw' signal, that is, the method that actually draws something to the graphics memory you just throw your "behind the scenes" image.
Theory in code (test.py):
import cairo
from gi.repository import Gtk
from os.path import abspath, dirname, join
WHERE_AM_I = abspath(dirname(__file__))
class MyApp(object):
"""Double buffer in PyGObject with cairo"""
def __init__(self):
# Build GUI
self.builder = Gtk.Builder()
self.glade_file = join(WHERE_AM_I, 'test.glade')
self.builder.add_from_file(self.glade_file)
# Get objects
go = self.builder.get_object
self.window = go('window')
# Create buffer
self.double_buffer = None
# Connect signals
self.builder.connect_signals(self)
# Everything is ready
self.window.show()
def draw_something(self):
"""Draw something into the buffer"""
db = self.double_buffer
if db is not None:
# Create cairo context with double buffer as is DESTINATION
cc = cairo.Context(db)
# Scale to device coordenates
cc.scale(db.get_width(), db.get_height())
# Draw a white background
cc.set_source_rgb(1, 1, 1)
# Draw something, in this case a matrix
rows = 10
columns = 10
cell_size = 1.0 / rows
line_width = 1.0
line_width, notused = cc.device_to_user(line_width, 0.0)
for i in range(rows):
for j in range(columns):
cc.rectangle(j * cell_size, i * cell_size, cell_size, cell_size)
cc.set_line_width(line_width)
cc.set_source_rgb(0, 0, 0)
cc.stroke()
# Flush drawing actions
db.flush()
else:
print('Invalid double buffer')
def main_quit(self, widget):
"""Quit Gtk"""
Gtk.main_quit()
def on_draw(self, widget, cr):
"""Throw double buffer into widget drawable"""
if self.double_buffer is not None:
cr.set_source_surface(self.double_buffer, 0.0, 0.0)
cr.paint()
else:
print('Invalid double buffer')
return False
def on_configure(self, widget, event, data=None):
"""Configure the double buffer based on size of the widget"""
# Destroy previous buffer
if self.double_buffer is not None:
self.double_buffer.finish()
self.double_buffer = None
# Create a new buffer
self.double_buffer = cairo.ImageSurface(\
cairo.FORMAT_ARGB32,
widget.get_allocated_width(),
widget.get_allocated_height()
)
# Initialize the buffer
self.draw_something()
return False
if __name__ == '__main__':
gui = MyApp()
Gtk.main()
Glade file (test.glade):
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<!-- interface-requires gtk+ 3.0 -->
<object class="GtkWindow" id="window">
<property name="can_focus">False</property>
<property name="window_position">center-always</property>
<property name="default_width">800</property>
<property name="default_height">600</property>
<signal name="destroy" handler="main_quit" swapped="no"/>
<child>
<object class="GtkDrawingArea" id="drawingarea1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<signal name="draw" handler="on_draw" swapped="no"/>
<signal name="configure-event" handler="on_configure" swapped="no"/>
</object>
</child>
</object>
</interface>
Dependencies:
Python 2:
sudo apt-get install python-cairo
Python 3:
sudo apt-get install python3-gi-cairo
Now execute with:
python test.py
or
python3 test.py
What it looks like:
All the documentation for cairo can be found in http://cairographics.org/documentation/pycairo/3/reference/index.html
Kind regards
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With