Simple image view with zoom and pan

Hi :slight_smile:

I’m currently trying to prepare a very simple image viewer component with gtk and vala, i’d like to have it zoom and pan features. So here is my current code implementation divided into two classes:

namespace Image {

    public class ImageViewerPanningArea : Gtk.Widget {

        construct {
            set_layout_manager(new Gtk.BinLayout());
            vexpand = hexpand = true;
        }

        private ImageViewer viewer;
        private Gtk.ScrolledWindow scrolled_window;
        private bool mouse_pressed = false;
        private double last_mouse_x;
        private double last_mouse_y;

        public ImageViewerPanningArea(ImageViewer viewer) {
            this.viewer = viewer;
            var dimensions = viewer.get_dimensions ();

            //  var hadjustment = new Gtk.Adjustment(0.0, 0.0, dimensions.width, 1.0, 10.0, 1.0);
            //  var vadjustment = new Gtk.Adjustment(0.0, 0.0, dimensions.height, 1.0, 10.0, 1.0);

            var hadjustment = new Gtk.Adjustment(0.0, 0.0, dimensions.width, 1.0, 10.0, dimensions.width);
            var vadjustment = new Gtk.Adjustment(0.0, 0.0, dimensions.height, 1.0, 10.0, dimensions.height);

            this.scrolled_window = new Gtk.ScrolledWindow ();
            scrolled_window.set_parent (this);
            scrolled_window.set_hadjustment (hadjustment);
            scrolled_window.set_vadjustment (vadjustment);
            scrolled_window.set_child (viewer);

            var button_controller = new Gtk.GestureClick ();
            scrolled_window.add_controller(button_controller);
            scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);


            button_controller.pressed.connect((button, x, y) => {
                if (button == 1) { 
                    mouse_pressed = true;
                    last_mouse_x = x;
                    last_mouse_y = y;
                }
            });

            button_controller.released.connect((button, x, y) => {
                if (button == 1) {
                    mouse_pressed = false;
                }
            });

            var motion_controller = new Gtk.EventControllerMotion ();
            scrolled_window.add_controller (motion_controller);
            motion_controller.motion.connect((x, y) => {
                if (mouse_pressed) {
                    stdout.printf("Mouse moved to (%f, %f)\n", x, y);

                    double dx = x - last_mouse_x;
                    double dy = y - last_mouse_y;

                    stdout.printf("DX: %f, DY: %f\n", dx, dy);
            
                    scrolled_window.hadjustment.set_value(scrolled_window.hadjustment.get_value() - dx);
                    scrolled_window.vadjustment.set_value(scrolled_window.vadjustment.get_value() - dy);
                    
                    last_mouse_x = x;
                    last_mouse_y = y;
                    scrolled_window.queue_draw ();
                }
            });


            var key_controller = new Gtk.EventControllerKey();
            scrolled_window.add_controller (key_controller);
    
            key_controller.key_pressed.connect((keyval, keycode, state) => {
                if (keyval == Gdk.Key.Control_L || keyval == Gdk.Key.Control_R) {
                    stdout.printf ("AAAAAAAAAAAAAAAAAAAAAAAAAAa\n");
                }
                return false;
            });



            //  viewer.dimensions_changed.connect(update_adjustments);
        }

        private void update_adjustments(ImageDimensions dimensions) {
            double imageWidth = dimensions.width;
            double imageHeight = dimensions.height;

            scrolled_window.hadjustment.set_upper(imageWidth);
            scrolled_window.vadjustment.set_upper(imageHeight);

            scrolled_window.hadjustment.set_page_size(scrolled_window.get_allocated_width());
            scrolled_window.vadjustment.set_page_size(scrolled_window.get_allocated_height());

        }
    }

    

    protected struct ImageDimensions {
        int width;
        int height;
    }

    public class ImageViewer : Gtk.DrawingArea {

        public signal void dimensions_changed(ImageDimensions new_dimensions);
        public signal void zoom_changed(double new_zoom_value);

        private const double ZOOM_TICK = 0.1;

        private Gdk.Pixbuf pixbuf;
        private Gdk.Texture texture;

        private double zoom_level = 1.0;
        public double zoom {
            get { return zoom_level; }
            set {
                zoom_level = value;
        
                int allocated_width = get_allocated_width();
                int allocated_height = get_allocated_height();
                
                int target_width = (int)(allocated_width * zoom_level);
                int target_height = (int)(allocated_height * zoom_level);
                
                this.set_size_request(target_width, target_height);
                this.queue_resize();
                this.queue_draw();
                zoom_changed(value);
            }
        }

        construct {
            hexpand = vexpand = true;
        }

        public ImageViewer(Gdk.Pixbuf pixbuf) {
            this.pixbuf = pixbuf;
            this.texture = Gdk.Texture.for_pixbuf (pixbuf);
        }
        
        public void zoom_in() {
            zoom += ZOOM_TICK; 
        }
        
        public void zoom_out() {
            if (zoom > ZOOM_TICK) {  
                zoom -= ZOOM_TICK;
            }
        }
        
        protected override void snapshot (Gtk.Snapshot snapshot) {
            if (texture == null) {
                return;
            }
        
            int width = get_allocated_width();
            int height = get_allocated_height();
        
            draw_checker_board(snapshot, width, height);
        
            double width_scale = (double)width / texture.get_width();
            double height_scale = (double)height / texture.get_height();
        
            // We take the smaller of the two to ensure the image fits within the widget's bounds.
            double scale_factor = (width_scale < height_scale) ? width_scale : height_scale;
        
            // Here we ensure the image doesn't grow larger than its natural size.
            scale_factor = (scale_factor > 1.0) ? 1.0 : scale_factor;
        
            double effective_scale = scale_factor * zoom_level;
        
            int scaled_width = (int)(texture.get_width() * effective_scale);
            int scaled_height = (int)(texture.get_height() * effective_scale);
        
            int x_offset = (width - scaled_width) / 2;
            int y_offset = (height - scaled_height) / 2;
        
            var rect = Graphene.Rect();
            rect.init(x_offset, y_offset, scaled_width, scaled_height);
            snapshot.append_texture(texture, rect);
        }

        private void draw_checker_board (Gtk.Snapshot snapshot, int width, int height) {
             // Define the size of each square in the checkerboard
            int square_size = 20;  // You can adjust this value as needed

            // Colors for the checkerboard
            Gdk.RGBA color1 = Gdk.RGBA() { red = 0.8f, green = 0.8f, blue = 0.8f, alpha = 0.6f }; // light gray
            Gdk.RGBA color2 = Gdk.RGBA() { red = 0.6f, green = 0.6f, blue = 0.6f, alpha = 0.6f }; // darker gray

            bool useFirstColor;
            
            for (int x = 0; x < width; x += square_size) {
                useFirstColor = (x / square_size) % 2 == 0;  // Alternate starting color for each row
                
                for (int y = 0; y < height; y += square_size) {
                    var rect = Graphene.Rect();
                    rect.init(x, y, square_size, square_size);

                    snapshot.append_color(useFirstColor ? color1 : color2, rect);

                    // Alternate the color for the next square in the row
                    useFirstColor = !useFirstColor;
                }
            }
        }


        protected override void measure (Gtk.Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) {
            minimum_baseline = natural_baseline = -1;
        
            if (pixbuf == null) {
                minimum = natural = 0;
                return;
            }
        
            var dimensions = get_dimensions ();
            // Here, we set minimum to a small value, not necessarily tied to the image size.
            if (orientation == Gtk.Orientation.HORIZONTAL) {
                minimum = 30;
                natural = dimensions.width;
            } else {
                minimum = 30;
                natural = dimensions.height;
            }
        }
        

        public ImageDimensions get_dimensions () {

            double width_scale = (double)pixbuf.get_width () / texture.get_width();
            double height_scale = (double)pixbuf.get_height () / texture.get_height();
        
            // We take the smaller of the two to ensure the image fits within the widget's bounds.
            double scale_factor = (width_scale < height_scale) ? width_scale : height_scale;
        
            // Here we ensure the image doesn't grow larger than its natural size.
            scale_factor = (scale_factor > 1.0) ? 1.0 : scale_factor;
        
            double effective_scale = scale_factor * zoom_level;
        
            int scaled_width = (int)(texture.get_width() * effective_scale);
            int scaled_height = (int)(texture.get_height() * effective_scale);


            return {
                width: scaled_width,
                height: scaled_height
            };
        }

        protected override void size_allocate (int width, int height, int baseline) {
            base.size_allocate(width, height, baseline);
            dimensions_changed({
                width: (int)(width * zoom),
                height: (int)(height * zoom)
            });
        }
    }  
}

You use it like this:

var pixbuf = new Gdk.Pixbuf.from_file("cat1.jpg");
var viewer = new Image.ImageViewer(pixbuf);
viewer.zoom = 2;
var viewer_pan = new Image.ImageViewerPanningArea(viewer);

box.append (viewer_pan);

I’m having lots of troubles with implementing panning using ScrolledWindow :frowning: I’m trying different combinations but still no luck - scrolledwindow detects that the image covers 100% of scrolling area and it’s preventing from even showing the scrollbars :frowning:

I appreciate any help here…

Allright, it seems I’ve found a way to do it :slight_smile:
I can share code for anyone interested on PM.

Cheers :slight_smile:

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.