Search code examples
c#monogtk#cairo

Simple paint application: Referencing Cairo Context on mouse movement


I am attempting to use Cairo with GTK# to create a simple painting application. The feature that is giving me trouble is the one of drawing a line between two points. After clicking on the drawing area, a line should appear, and follow the cursor until the mouse button is released, at which point the drawing area is updated and another line can be drawn. It should be a familiar feature from programs such as MS Paint.

Now, I am told the way to perform such updates in Cairo is to use the Cairo Context's .Save(); and .Restore() methods to handle the states. Problem is that I can't find a way to continue to reference the same Context in an event handler other than the one that created it, so all my attempts to restore the state after drawing a line have been less than fruitful.

In the minimum (but still kind of humongous, sorry) working code example below, the non-solution is to create a new Context on each Draw. This gives results as below: Current state of the Painting application

The pen (on the left) behaves all right. The line obviously doesn't, as there's no way to restore to the previous state after moving the mouse, so they all remain on the screen to make the pictured fan-pattern.

using Gtk;
using Cairo;
using System;

public class Paint : Gtk.Window
{

    bool isDrawing = false;
    bool isDrawingLine = false;
    bool isDrawingWithPen = false;

    double Point_l1x;
    double Point_l1y;
    double Point_l2x;
    double Point_l2y;

    double Point_p1x;
    double Point_p1y;

    Button line;
    Button pen;

    static DrawingArea area;
    Cairo.Context ctx;

    void OnDrawingAreaExposed (object source, ExposeEventArgs args)
    {   
        DrawingArea area = (DrawingArea) source;
        ctx = Gdk.CairoHelper.Create (area.GdkWindow);

        ((IDisposable) ctx.Target).Dispose();
        ((IDisposable) ctx).Dispose ();
    }

    public void DrawImage ()
    {
        //Shouldn't this be referencing an external context?
        using (Cairo.Context ctx = Gdk.CairoHelper.Create (area.GdkWindow))
        {
            ctx.Color = new Cairo.Color (0, 0, 0);

            if(isDrawingLine)
            {
                ctx.MoveTo (new PointD (Point_l1x, Point_l1y));
                ctx.LineTo (new PointD (Point_l2x, Point_l2y));
                ctx.Stroke ();
            }

            else if(isDrawingWithPen)
            {
                ctx.Rectangle(Point_p1x, Point_p1y, 1, 1);
                ctx.Stroke();
            }
        }
    }

    public void LineClicked(object sender, EventArgs args)
    {
        isDrawingLine = true;
        isDrawingWithPen = false;
    }

    public void PenClicked(object sender, EventArgs args)
    {
        isDrawingLine = false;
        isDrawingWithPen = true;
    }

    void OnMousePress (object source, ButtonPressEventArgs args)
    {
        isDrawing = true;
        if (isDrawingLine)
        {
            Point_l1x = args.Event.X;
            Point_l1y = args.Event.Y;
        }
        else if (isDrawingWithPen)
        {
            Point_p1x = args.Event.X;
            Point_p1y = args.Event.Y;
        }
    }

    void OnMouseRelease (object source, ButtonReleaseEventArgs args)
    {
        isDrawing = false;
        DrawImage ();
    }

    void OnMouseMotion (object source, MotionNotifyEventArgs args)
    {
        if (!isDrawing)
        {
            return;
        }

        if (isDrawingLine)
        {
            Point_l2x = args.Event.X;
            Point_l2y = args.Event.Y;
        }
        else if (isDrawingWithPen)
        {
            Point_p1x = args.Event.X;
            Point_p1y = args.Event.Y;
        }
        DrawImage();
    }

    public Paint () : base("Painting application")
    {   
        area = new DrawingArea ();
        area.ExposeEvent += OnDrawingAreaExposed;

        area.AddEvents (
                (int)Gdk.EventMask.PointerMotionMask
                | (int)Gdk.EventMask.ButtonPressMask
                | (int)Gdk.EventMask.ButtonReleaseMask);

        area.ButtonPressEvent += OnMousePress;
        area.ButtonReleaseEvent += OnMouseRelease;
        area.MotionNotifyEvent += OnMouseMotion;

        DeleteEvent += delegate { Application.Quit(); };

        SetDefaultSize(500, 500);
        SetPosition(WindowPosition.Center);

        VBox vbox = new VBox();
        vbox.Add(area);
        HBox hbox = new HBox();

        line = new Button("Line");
        pen = new Button("Pen");
        hbox.Add(line);
        hbox.Add(pen);

        Alignment halign = new Alignment(1, 0, 0, 0);
        halign.Add(hbox);

        vbox.Add(hbox);
        vbox.PackStart(halign, false, false, 3);

        line.Clicked += LineClicked;
        pen.Clicked += PenClicked;

        Add(vbox);
        ShowAll();
    }

    public static void Main()
    {
        Application.Init();
        new Paint();
        Application.Run();
    }
}

If I modify the DrawImage method to reference the Context defined in OnDrawingAreaExposed, the whole thing crashes, offering a stack trace that I can't really understand:

Stacktrace:

at (wrapper managed-to-native) Cairo.NativeMethods.cairo_set_source_rgba (intptr,double,double,double,double) <0xffffffff>
at Cairo.Context.set_Color (Cairo.Color) <0x0002b>
at Paint.DrawImage () <0x000a3>
at Paint.OnMouseMotion (object,Gtk.MotionNotifyEventArgs) <0x001af>
at (wrapper runtime-invoke) <Module>.runtime_invoke_void__this___object_object (object,intptr,intptr,intptr) <0xffffffff>
at (wrapper managed-to-native) System.Reflection.MonoMethod.InternalInvoke (System.Reflection.MonoMethod,object,object[],System.Exception&) <0xffffffff>
at System.Reflection.MonoMethod.Invoke (object,System.Reflection.BindingFlags,System.Reflection.Binder,object[],System.Globalization.CultureInfo) <0x0018b>
at System.Reflection.MethodBase.Invoke (object,object[]) <0x0002a>
at System.Delegate.DynamicInvokeImpl (object[]) <0x001a3>
at System.MulticastDelegate.DynamicInvokeImpl (object[]) <0x0003b>
at System.Delegate.DynamicInvoke (object[]) <0x00018>
at GLib.Signal.ClosureInvokedCB (object,GLib.ClosureInvokedArgs) <0x0014f>
at GLib.SignalClosure.Invoke (GLib.ClosureInvokedArgs) <0x0002f>
at GLib.SignalClosure.MarshalCallback (intptr,intptr,uint,intptr,intptr,intptr) <0x0050b>
at (wrapper native-to-managed) GLib.SignalClosure.MarshalCallback (intptr,intptr,uint,intptr,intptr,intptr) <0xffffffff>
at (wrapper managed-to-native) Gtk.Application.gtk_main () <0xffffffff>
at Gtk.Application.Run () <0x0000b>
at Paint.Main () <0x00027>
at (wrapper runtime-invoke) object.runtime_invoke_void (object,intptr,intptr,intptr) <0xffffffff>

Native stacktrace:

        mono() [0x4961e9]
        mono() [0x4e6d1f]
        mono() [0x41dcb7]
        /lib/x86_64-linux-gnu/libpthread.so.0(+0xfcb0) [0x7f3fd2f07cb0]
        /usr/lib/x86_64-linux-gnu/libcairo.so.2(cairo_set_source_rgba+0x1) [0x7f3fcaaccc71]
        [0x41a28dbd]

Am I on the right track here, trying to reference that Context? Do Cairo Contexts even work that way? If not, how can I make the line continuously re-render?


Solution

  • Future visitors may be interested in knowing that I did end up plastering together a solution to this. Note the use of delegation. Results can be found below:

    Screen shot: The working application

    Source code:

    using Gtk;
    using Cairo;
    using System;
    
    public class Paint : Gtk.Window
    {
        delegate void DrawShape(Cairo.Context ctx, PointD start, PointD end);
    
        ImageSurface surface;
        DrawingArea area;
        DrawShape Painter;
        PointD Start, End;
    
        bool isDrawing;
        bool isDrawingPoint;
    
        Button line;
        Button pen;
    
        public Paint() : base("Painting application")
        {
            surface = new ImageSurface(Format.Argb32, 500, 500);
            area = new DrawingArea();
    
            area.AddEvents(
                (int)Gdk.EventMask.PointerMotionMask
                |(int)Gdk.EventMask.ButtonPressMask
                |(int)Gdk.EventMask.ButtonReleaseMask);
    
            area.ExposeEvent += OnDrawingAreaExposed;
            area.ButtonPressEvent += OnMousePress;
            area.ButtonReleaseEvent += OnMouseRelease;
            area.MotionNotifyEvent += OnMouseMotion;
            DeleteEvent += delegate { Application.Quit(); };
    
            Painter = new DrawShape(DrawLine);
    
            Start = new PointD(0.0, 0.0);
            End = new PointD(500.0, 500.0);
            isDrawing = false;
            isDrawingPoint = false;
    
            SetDefaultSize(500, 500);
            SetPosition(WindowPosition.Center);
    
            VBox vbox = new VBox();
            vbox.Add(area);
            HBox hbox = new HBox();
    
            line = new Button("Line");
            pen = new Button("Pen");
            hbox.Add(line);
            hbox.Add(pen);
    
            Alignment halign = new Alignment(1, 0, 0, 0);
            halign.Add(hbox);
    
            vbox.Add(hbox);
            vbox.PackStart(halign, false, false, 3);
    
            line.Clicked += LineClicked;
            pen.Clicked += PenClicked;
    
            Add(vbox);
    
            Add(area);
            ShowAll();
        }
    
        void OnDrawingAreaExposed(object source, ExposeEventArgs args)
        {
            Cairo.Context ctx;
    
            using (ctx = Gdk.CairoHelper.Create(area.GdkWindow))
            {
                ctx.Source = new SurfacePattern(surface);
                ctx.Paint();
            }
    
            if (isDrawing)
            {
                using (ctx = Gdk.CairoHelper.Create(area.GdkWindow))
                {
                    Painter(ctx, Start, End);
                }
            }
        }
    
        void OnMousePress(object source, ButtonPressEventArgs args)
        {
            Start.X = args.Event.X;
            Start.Y = args.Event.Y;
    
            End.X = args.Event.X;
            End.Y = args.Event.Y;
    
            isDrawing = true;
            area.QueueDraw();
        }
    
        void OnMouseRelease(object source, ButtonReleaseEventArgs args)
        {
            End.X = args.Event.X;
            End.Y = args.Event.Y;
    
            isDrawing = false;
    
            using (Context ctx = new Context(surface))
            {
                Painter(ctx, Start, End);
            }
    
            area.QueueDraw();
        }
    
        void OnMouseMotion(object source, MotionNotifyEventArgs args)
        {
            if (isDrawing)
            {
                End.X = args.Event.X;
                End.Y = args.Event.Y;
    
                if(isDrawingPoint)
                {
                    using (Context ctx = new Context(surface))
                    {
                        Painter(ctx, Start, End);
                    }
                }
                area.QueueDraw();
            }
        }
    
        void LineClicked(object sender, EventArgs args)
        {
            isDrawingPoint = false;
            Painter = new DrawShape(DrawLine);
        }
    
        void PenClicked(object sender, EventArgs args)
        {
            isDrawingPoint = true;
            Painter = new DrawShape(DrawPoint);
        }
    
        void DrawLine(Cairo.Context ctx, PointD start, PointD end)
        {
            ctx.MoveTo(start);
            ctx.LineTo(end);
            ctx.Stroke();
        }
    
        void DrawPoint(Cairo.Context ctx, PointD start, PointD end)
        {
            ctx.Rectangle(end, 1, 1);
            ctx.Stroke();
        }
    
        public static void Main()
        {
            Application.Init();
            new Paint();
            Application.Run();
        }
    }