Hi
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 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
I appreciate any help here…